diff --git a/2022/12/21/JavaWeb/index.html b/2022/12/21/JavaWeb/index.html index 2928b5d6..a3e43bf4 100644 --- a/2022/12/21/JavaWeb/index.html +++ b/2022/12/21/JavaWeb/index.html @@ -2171,7 +2171,7 @@
向量处理器意思是一条指令可以同时处理多个数据元素(SIMD)(就类似于这几个数据元素组成了一个向量);多发射处理器可以同一时间并行多条指令。
-发射与流出
-在计算机体系结构中,”发射”和”流出”是与指令执行有关的两个重要概念,它们描述了处理器在执行指令时的不同阶段和行为。
+JUnit是白盒测试。
+包含各种测试用例。
+一般放在包名xxx.xxx.xx.test里,类名为“被测试类名Test”。
+测试方法可以独立运行。
+方法名一般为“test测试的方法”,void,空参。
+Assert.assertEquals(3,result); |
@Before在所有测试方法执行前自动执行,常用于资源申请。
+@After在所有测试方法执行完后自动执行,常用于释放资源。
+反射是框架设计的灵魂。
+类加载器把硬盘中的字节流文件装载进内存,并且翻译封装为Class类对象。通过Class类对象才能创建Person对象。
+而这也就是说,如果我们有了Class对象,我们就可以创建该类对象。
+有三种方式。
+将字节码文件加载进内存,返回class对象。多用于配置文件【将类名定义在配置文件】
+注意:类的全名指的是包.类,包含包名。
+通过类名的属性class获取。多用于参数传递。
+getClass()是Object类的方法。多用于对象的获取字节码的方式。
+//第一种方式 |
同一个字节码文件(*.class)在一次程序运行过程中只会被加载一次,不管是以哪种方式得到的Class对象,都是同一个。
+可以通过class对象得到其字段、构造方法、方法等。
+class Student{ |
Student stu = new Student("张三",321,1000,57.7); |
常用方法:
+//获取所有公有字段 |
Field f = stuC.getDeclaredField("money"); |
获取方法跟上面格式差不多。
+//获取构造方法 |
如果想要获取公有的无参构造器,还可以使用Class类提供的更简单的方法,不用先创造构造器:
+System.out.println(stuC.newInstance()); |
//获取方法 |
public class ReflectTest { |
①和③都是jdk预定义的。自定义主要是②。
+javadoc XXX.java |
会自动根据里面的注解生成文档
+
|
本质上
+public Override{} |
等价于
+public interface Override extends java.lang.annotation.Annotation{} |
注解的属性就是接口中的成员方法。要求无参,且返回类型有固定取值:
+public MyAnno { |
|
描述注解的注解
+RetentionPolicy的三个取值:SOURCE、CLASS、RUNTIME,正对应着java对象的三个阶段。
+SOURCE:不保留到字节码文件,会被编译器扔掉
+CLASS:保留到字节码文件
+RUNTIME:被读到
+自定义的注解一般都取RUNTIME。
+相当于用注解替换配置文件
+
|
|
class.getAnnotation(Pro.class);
这句话实质上是创建了一个实例,继承了Pro接口,重载了里面的抽象方法。
|
然后在要测试的每个方法上面加上此标签。
+然后编写test方法:
+public class TestCheck { |
mysql -h[IP地址] -u[用户名] -p |
本地的一个文件夹就代表一个数据库,文件夹里的一个文件代表一张表。
+SQL有四种语句类型
+create datebase 数据库名称; |
drop database 数据库名称; |
alter database 数据库名称 charactor set 修改后新值; |
show databases;# 查询所有数据库名称 |
select database();# 查询正在使用的数据库名称 |
create table students( |
*注:
mysql的数据类型表
+其中:
+① double(3,1)表示XXX.X,最大值为99.9.
+② 关于三个时间类型
+所以timestamp常用作插入时间。
+③ varchar(20)表示二十个字符长的字符串。
+注意,是“二十个字符”而不是“二十个字节”。如果使用的字符集每个字符占3个字节,则varchar(20)占60个字节。
+④ BLOB、CLOB、二进制这些用于存储大数,不常用
这两个概念都涉及到提高指令级并行性,但它们描述了处理器在执行阶段的不同方面。发射强调在同一时钟周期内同时发送多条指令,而流出强调在执行过程中的乱序执行策略。
-tensor 张量
-sparse tensor 稀疏张量
-异构计算
-指的是在同一系统中集成多种不同体系结构或架构的处理器和计算设备,以便更有效地处理各种类型的任务。这包括集成不同类型的中央处理单元(CPU)、图形处理单元(GPU)、加速器、协处理器等。异构计算的目标是充分发挥各种处理器的优势,以提高整体系统性能和能效。
-其关键概念有协处理器等等等。
+drop table 表名; |
# 修改表名 |
show tables;# 查询数据库中所有表的名字 |
insert into students(name,age,score,birthday) values('张三',15,99.9,"2022-12-5"); |
如果不加条件,会把表中所有数据删除
+delete from students where name="张三"; |
如果不加条件,会把表中所有记录全部修改
+update students set name="1", age=10 where name="张三"; |
select # 多字段查询 |
基本运算符
+<、>、=、<=、>=、<>(不等于,也可以用!=)
注意几点:
-注意,它的意思是LD、SD、DADDIU都只占1个时钟周期,ADD占2个
-感觉这么个例题下来,我就懂了循环展开的作用了
-这里的结构相关值得注意
-做这种题的套路是,需要明确它要求的时刻时的情况,并且依照以下规则判断即可:
-指令状态表
-流出
-无结构冲突、无WAW冲突
-如① 当MULT准备写回时,此时前两条L必定流出,然后后面的SUB、DIV、ADD都没有结构冲突和WAW冲突,所以全部流出。只不过ADD和DIV会卡在读操作数阶段
-② 由①可知全部流出
+逻辑运算符
+AND、OR
读操作数
-操作数可用时完成该阶段
-如① 此时前三条必定完成。并且SUB也完成了,所以ADD也完成了读数阶段。只有DIV还在等待mul的结果
-② 此时大伙差不多都结了,没什么好说的
+BETWEEN AND
执行
-纯纯的算术
-如① 除了除法别的都完了,没什么好说的
-② 全部都结了
+IN后跟集合
+写结果
-不存在WAR则写入
-如① 前两个肯定完成了,然后SUB也结了,ADD存在WAR,所以最后是ADD和MUL没完成。
-② 除了DIV全部结了
+IS、IS NOT
+LIKE 模糊查询
+类似正则使用占位符匹配
+select * from students where name like "马%"; |
order by 排序字段1 排序方式1,排序字段2 排序方式2; |
默认升序。
功能部件状态表
-记住这些字母的含义即可:
-ASC、DESC
寄存器状态表
-每个寄存器有一项,用于指出哪个功能部件将把结果写入
+多关键字排序
+第二条件仅当第一条件一样才使用。
3段流水
-流出
+将一列数据作为整体,纵向计算
+注意,聚合函数的计算会排除NULL值。如果不想让空置排除,可以尝试该方法:
+select count(ifnull(math,0)) from students; |
没有结构冲突就流出,填进保留站
-一般有ADD1,ADD2,ADD3(加减),MUL1,MUL2(乘除),LD1,LD2(SL)
+count 计算个数
+select count(name) from students;# 有多少条记录 |
一般如果要看有多少记录,可以用count(主键),因为主键不为空。
具体填什么看操作数有没有就绪
+max、min
保留站有以下字段:
-Op:操作
-Qj,Qk:操作数保留站号
-Vj,Vk:源操作数值
-load的Vk保存偏移量
-Busy
-A:存放立即数字段 or 有效地址,仅用于load和store缓冲器
-Qi:寄存器状态表
-存放要写入它的保留站ID
-执行
-两个操作数就绪后就执行
+sum 求和
写结果
-计算完毕后由CDB传送
+avg 平均值
这里不知道为什么LD2没有跟LD1同时完成?限制了一个时钟周期只流出一条指令吗
-这里可以注意其特点是结果一经算出全部写回
-通过换名避免了WAR,而不是像记分牌那样通过等待
-4段流水
-流出
-保留站&ROB都有空闲才流出
-一般有ADD1,ADD2,ADD3(加减),MUL1,MUL2(乘除),LD1,LD2(SL)
-具体填什么看操作数有没有就绪
-分组之后查询的字段只能是两种:① 分组字段 ② 聚合函数。因为分组了之后再查具有个人特色的东西就没意义了。【高版本的mysql如果查询别的字段会报错】
+select sex,avg(math) from students group by sex; |
还可以在分组前对条件限定,使用WHERE
+select sex,avg(math) from students where math >= 70 group by sex; |
或者在分组后限定,使用HAVING
+select sex,avg(math),count(id) from students group by sex having count(id)>2; |
保留站有以下字段:
-Op:操作
+这种就是分页查询。
+limit 开始的索引,每页查询的条数; |
limit
只能在mysql使用。
用户表存放地点↑
+USE mysql; |
注意,以下出现的”用户名”@”主机名” IDENTIFIED BY “密码”,不能在@两侧加空格,否则报错。
+CREATE USER "用户名"@"主机名" IDENTIFIED BY "密码"; |
DROP USER "用户名"@"主机名"; |
-- 使用mysql自带的密码加密函数PASSWORD |
SHOW GRANTS FOR "root"@"%"; |
grant 权限列表 on 数据库名.表名 to '用户名'@'主机名'; |
revoke 权限列表 on 数据库名.表名 from '用户名'@'主机名'; |
CREATE TABLE stu( |
如果要去掉该约束,可以这么做:
+ALTER TABLE stu MODIFY name VARCHAR(20); |
由于我们没写“NOT NULL ”,所以非空约束就被去掉了。感觉这点的解释挺有意思的。
+某列值不能重复
+CREATE TABLE stu( |
但是注意,唯一约束允许多个NULL存在。
+唯一约束的删除方法跟前面的非空约束就完全不一样了。
+ALTER TABLE stu DROP INDEX phone_number; |
++创建唯一约束时会自动创建唯一索引,需要删除索引
+
一张表只能有一个主键。主键非空且唯一。
+CREATE TABLE stu( |
ALTER TABLE stu DROP PRIMARY KEY; |
这东西一般都跟主键结合使用。
+若某一列是数值类型,可以使用auto_increment关键字来完成值的自动增长。
+CREATE TABLE stu( |
自动增长的数据只跟上一个记录有关系。
+表中dep_name和dep_location有数据冗余,修改或者插入都不方便,不符合数据库设计准则,所以需要创造两张表。
+但要是你想裁员了,直接在第二个表删研发部是没用的,第一个表数据还在,还得麻烦地一个个删。这时候外键就起作用了。
+外键只能关联唯一约束或者主键约束的列。一般外键都是去关联主表的主键。
+CREATE TABLE employee( |
此时不能删除department的行【要是该行在employee表没出现过的话就可以删掉】,也不能在employee添加一个不存在的外键值。
+ALTER TABLE employee DROP FOREIGN KEY emp_dept_fk; |
如果你想修改外表主键值,就需要用到级联更新。
+ALTER TABLE employee ADD CONSTRAINT emp_dept_fk FOREIGN KEY (dep_id) REFERENCES department(id) ON UPDATE CASCADE ;-- 外键声明+级联更新声明 |
如果你想达到删除一个主键值就能删除表中的所有与该主键值关联的数据,就需要用到级联删除。
+ALTER TABLE employee ADD CONSTRAINT emp_dept_fk FOREIGN KEY (dep_id) REFERENCES department(id) ON DELETE CASCADE ;-- 外键声明+级联删除声明 |
级联使用应该要谨慎。一是它不大安全,二是它涉及多表操作,效率低下
+1NF中的主属性为学号和课程名称。可以看到,分数完全依赖于码,但是姓名、系名、系主任都只是部分依赖于码,这不符合2NF的条件。因而,我们就可以选择拆分表,把完全依赖的部分和部分依赖的部分分开:
+由于分数->(学号,课程名称),因而可以把学号、课程名称、分数放在一张表
+由于姓名、系名、系主任 ->(学号),因而可以把学号、姓名、系名、系主任放在一张表
+如下图所示。这样就消除了部分依赖。
+2NF中选课表的主属性为学号和课程名称,学生表的主属性为学号。可以看到,学生表中,存在着系主任->系名->学号这样的传递依赖,不符合3NF的规定。因而,我们需要对学生表进行进一步的拆分。
+我们为了破坏系主任->系名->学号这个传递链,可以拆分成系主任->系名和系名->学号两个传递关系。
+因而,可以把学生表拆分为如下图两张表:
+使用where条件
+-- 查询所有员工信息和对应的部门信息 |
++此在书中称为“等值连接和非等值连接”。
+
语法: select 字段列表 from 表名1 [inner] join 表名2 on 条件
+SELECT * FROM emp INNER JOIN dept ON emp.`dept_id` = dept.`id`; |
++关于外连接和内连接的区别,以及左外连接与右外连接的区别:
++
![屏幕截图 2022-12-19 155750](./JavaWeb/屏幕截图 2022-12-19 155750.png)
++
语法:select 字段列表 from 表1 left [outer] join 表2 on 条件;
+SELECT t1.*,t2.`name` FROM emp t1 LEFT JOIN dept t2 ON t1.`dept_id` = t2.`id`; |
语法:select 字段列表 from 表1 right [outer] join 表2 on 条件;
+SELECT * FROM dept t2 RIGHT JOIN emp t1 ON t1.`dept_id` = t2.`id`; |
查询嵌套
+++子查询中不允许使用ORDER BY
+在实际运用中,内连接比子查询的效率更高
+
+++
可用于WHERE条件
+-- 查询工资最高的员工信息 |
可以作为条件用IN关键字
+-- 查询'财务部'和'市场部'所有的员工信息 |
可以当做一个新表,可以转化为普通内连接
+-- 查询员工入职日期是2011-11-11日之后的员工信息和部门信息 |
+++
子查询内部使用了父查询的东西
++
+
一个包含多个步骤的业务操作被事务管理,操作要么同时成功,要么同时失败。【有种原子操作的感觉?】
+当操作失败时,会回滚到执行前的状态。
+事实上就是类似有个缓冲区,得到commit指令就把缓冲区内容更新,得到rollback指令就把缓冲区内容丢弃。
+-- 开启事务 |
-- 开启事务 |
START TRANSACTION; |
ROLLBACK; |
COMMIT; |
一条DML(增删改表中数据)语句默认会自动提交。但如果手动开启了事务,那么事务内保护的原子序列就需要手动提交。
+如果想将默认提交给kill了,也即不论是否开启事务都得手动提交,那么就需要用如下语句:
+SET @@autocommit = 0; |
一旦事务提交/回滚,会持久性更新数据库表。
+多个事务之间应该相互独立。为了保障这一点,需要设置事务的隔离级别。
+事务操作前后数据总量不变。
+概念:多个事务之间隔离的,相互独立的。但是如果多个事务操作同一批数据,则会引发一些问题,设置不同的隔离级别就可以解决这些问题。【有点并发的感觉】
+隔离级别越高,安全性越高,效率越来越差。
+mysql默认的是3,oracle默认的是2.
+可以设置隔离级别。
+set global transaction isolation level "级别字符串"; |
++通过之后老师说的内容,感觉有了点个人的感悟:
+级别1寻找数据可能优先从缓冲区找;级别2相当于不能读到缓冲区内容;级别3可能相当于在开启事务前对表做了个快照?级别4应该就是直接上了把互斥锁,同一时刻只能一个事务读写。
+
Java Database Connectivity Java语言操作数据库
+导入驱动jar包
+① 新建libs目录
+② 把jar包复制到libs目录下
+③ 右键libs目录 add as library
Qj,Qk:操作数保留站号
+注册驱动
Vj,Vk:源操作数值
-load的Vk保存偏移量
+获取数据库连接对象 Connection
Busy
+定义sql语句
A:存放立即数字段 or 有效地址,仅用于load和store缓冲器
+获取执行sql语句的对象 Statement
Qi:寄存器状态表
-存放要写入它的保留站ID
+执行sql,接收返回的结果
处理结果
执行
-两个操作数就绪后就执行
+释放资源
写结果
-ROB字段:
-指令类型
-目标地址
-目标寄存器/存储器单元地址
-数据值字段
-前瞻结果
-就绪字段
-结果是否就绪
-指令确认
-分支结果出来后确认
+public class JdbcDemo1 { |
优化版【增加try-catch-finally】:
+public class JdbcDemo1 { |
目的是告诉程序该使用哪一个数据库驱动jar包
+在快速入门中,我们使用这一行来注册驱动:
+Class.forName("com.mysql.jdbc.Driver"); |
表面上看跟DriverManager类可以说是毫无关系。
+但其实,类加载器加载类的时候,其实是会自动执行类中的静态代码块的。Driver类中有一段静态代码块如下:
+static { |
可见,注册驱动其实主要任务是由DriverManager类干的。这个静态块仅仅用于简化代码书写。
+++注意:mysql5之后的版本,这一步注册驱动可以省略。
++
配置文件里自动帮你注册了。我想原理应该是让本文件的类自动加载。
+
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/helloworld","root","root"); |
url的语法:”jdbc:mysql://IP地址:端口号/数据库名称”
+数据库连接对象。
+Statement createStatement() throws SQLException; |
void setAutoCommit(boolean autoCommit) throws SQLException; |
设置参数为false即开启事务。也即关闭自动提交。
+void commit() throws SQLException; |
void rollback() throws SQLException; |
++The object used for executing a static SQL statement and returning the results it produces.执行静态sql
+
//执行任意语句 |
封装查询结果集。
+具体取数方法就是类似迭代器原理。next移动迭代器指针,getXxx()方法,Xxx是数据类型,得到该行表记录中对应列对应数据类型的值。可以传入列数或者列名。
+boolean next() throws SQLException; |
使用实例:
+public static Collection<Client> query(){ |
注意:
这东西也得Close。
如果想做“查询到了结果则返回true”这样的操作,不应该使用这样的代码:
+if (resultSet != null) return true; |
而应该这样:
+return resultSet.next(); |
注意,SD指令的0和R1有了就开始执行,不必等到F4有了再执行。。。
-具体步骤:
+//An object that represents a precompiled SQL statement. |
Statement的子类。可以用来解决sql注入问题。
+它是预编译的sql语句,也即sql语句中的参数使用“?”占位符,需要传入参数。
+如下面的验证密码程序。键盘输入账号密码,从数据库查询该用户是否存在。
+public class Login { |
它更安全且效率更高。
+public class JDBCUtils { |
public static Collection<Client> query(){ |
使用Connection对象的管理事务的方法。
+public class Account { |
其实就是上面的JDBC中的Connection的对象池。
+非常简单,就是改一下Connection的获取,写一下xml就行。
+固定放在src目录下。名字必须为c3p0-config.xml或者c3p0.properties
+<c3p0-config> |
可以注意到,xml文件里面可以保存多套配置,比如上面的示例代码就保存了两套配置,default-config和name=”otherc3p0”的config。
+ComboPooledDataSource有一个含参构造器:
+public ComboPooledDataSource(String configName) { |
就可以传入config的名称指定要用的配置信息。
+DataSource cpds = new ComboPooledDataSource(); |
Druid的配置文件可以放在任意路径下,随便取名字。因为到时候需要指定配置文件。使用的是Properties文件。
+driverClassName=com.mysql.jdbc.Driver |
//导入配置文件 |
一般使用的时候还是会自定义一个工具类的
+import com.alibaba.druid.pool.DruidDataSourceFactory; |
使用同上的JDBCUtils
+Spring框架对JDBC的简单封装,提供JDBCTemplate对象。
+jdbcTemplate.update("update usr set money = ? where uname = ?",10,"Mary"); |
public static void main(String[] args) throws Exception { |
提供了三种方法。
+将得到的结果(只能是一行)封装为一个Map<String,Object>,其中key为列名,value为该行该列的值。
+如果得到的结果不为1行(=0 or >1),会抛出异常。
+JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource()); |
将得到的结果封装为List<Map<String,Object>>,其中一个Map为一行,多个Map表示多行,存储在List中。
+List<Map<String, Object>> res = jdbcTemplate.queryForList("select * from usr"); |
可以把查询回的结果封装为自己想要的对象而不是Map。如示例就封装为了Client对象。
+可以看到,里面的包装内容还是得自己写,有点麻烦。
+JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource()); |
使用包装好的BeanPropertyRowMapper类。
+JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource()); |
注意:
看起来大概可能就有点类似树的概念,什么都不依赖的就放前面,然后依赖1层的依赖2层的之类的
-开发指令级并行ILP的方法
+返回查到的某个东西。可以用于聚合函数的查询。
+int money = jdbcTemplate.queryForObject("select money from usr where uname = 'Mary'",Integer.class); |
JavaWeb:
+软件架构:
流水线CPI
-实际CPI = 理想CPI + 停顿(结构/数据/控制冲突引起)
-理想CPI是衡量流水线最高性能
-IPC:每个时钟周期完成的指令数
-CPI:每个指令所需时钟周期数
+基本程序块:一串没有分支和跳转转入点的指令块
+数据相关不能并行,需要插入暂停解决冲突
+解决方法
+B/S架构详解
+资源分类:
保持相关但避免冲突
-调度
+变换代码消除相关关系
+检测方法
-流经寄存器时直观;流经存储器复杂
+名相关
-分类
-解决方法:换名技术,可以编译器静态实现 or 硬件动态实现
-相关问题
-寄存器换名可以消除WAR和WAW冲突
-我们要学习动态资源,必须先学习静态资源!
数据冲突
-注意这里的命名,是按照正确顺序命名的。比如说RAW(read after write),写后读,正确次序就是i写入然后j再读,所以叫写后读。
-RAW(数据相关)
-也即i写j读
+静态资源:
+WAW(输出相关)
-也即i写j写
-流水线发生条件:流水线不止一个段可以写操作、指令被重新排序
-5段流水线不会发生,因为只会在WB阶段写寄存器
+WAR(反相关)
-也即i读j写
-流水线发生条件:有些指令写操作提前有些读操作滞后、指令被重新排序
+ +
|
布局
+页面布局使用table标签。这点让我感觉非常新奇。
+而且表格布局可以嵌套,也即每一行可以是一个新的表格。
图片适应屏幕宽度
+只需在img标签加个width=”100%”的属性即可。如:
+<img src="./image/top_banner.jpg" width="100%"> |
表项中的数据要被提交的话,必须指定其名称
+也即一定要有属性name。
控制相关
-由分支指令引起
+关于from的属性
+这里可能意思是引出了多流出,所以会导致DIV和ADD同时流出,从而发生WAW。同理,可能的阻塞也会导致WAR。
-基本思想
-在没有结构冲突时,尽可能早地执行没有数据冲突的指令,实现每个时钟周期执行一条指令
+一般都这么写
+基本结构
-三张表:指令执行状态、功能部件状态、寄存器状态及数据相关关系
-指令状态表
-记录正在执行的各条指令的状态
+
|
|
*{ |
1. 创建:
+ 1. var fun = new Function(形式参数列表,方法体); //忘掉吧
+ 2. function 方法名称(形式参数列表){
+ 方法体
+ }
+
+ 3. var 方法名 = function(形式参数列表){
+ 方法体
+ }
+2. 方法:
+
+3. 属性:
+ length:代表形参的个数
+4. 特点:
+ 1. 方法定义是,形参的类型不用写,返回值类型也不写。
+ 2. 方法是一个对象,如果定义名称相同的方法,会覆盖
+ 3. 在JS中,方法的调用只与方法的名称有关,和参数列表无关
+ 4. 在方法声明中有一个隐藏的内置对象(数组),**arguments**,封装所有的实际参数
+5. 调用:
+ 方法名称(实际参数列表);
+
+/** |
特点:全局对象,这个Global中封装的方法不需要对象就可以直接调用。 方法名();
功能部件状态表
-记录各个功能部件状态,每项有以下字段:
+方法:
encodeURI():url编码
decodeURI():url解码
encodeURIComponent():url编码,编码的字符更多
decodeURIComponent():url解码
parseInt():将字符串转为数字
结果寄存器状态表
-每个寄存器有一项,用于指出哪个功能部件将把结果写入
-大概是这样的结果:n(寄存器数量) X m(功能部件数量) 的值为0 or 1的矩阵
-eval():讲 JavaScript 字符串,并把它作为脚本代码来执行。
+var str = "http://www.baidu.com?wd=传智播客"; |
getElementById(); |
createAttribute(name); |
removeAttribute(); |
说了树结构后,这个就好理解多了。
+
|
老师标答值得学习借鉴的点:
+//使用innerHTML添加 |
|
|
浏览器对象模型,将浏览器各个组成部分封装成对象。
+Window对象包含DOM对象。
+组成:Window、Navigator、Screen、History、Location
+不需要创建,直接用window.使用,也可以直接用方法名。比如alert
+与弹出有关的方法
+alert:弹出警告框; confirm:确认取消对话框。确定返回true;prompt:输入框。参数为输入提示,返回值为输入值。
执行流程
-每条指令的执行过程分为4段(只考虑浮点计算)
-流出
-如果①所需功能部件空闲(结构冲突) ②其他正在执行指令目的寄存器与当前不同(WAW冲突),则流出
+与开关有关的方法
+close:关闭调用的window对象的浏览器窗口;open:打开新窗口,可传入URL,返回新的window对象
读操作数
-记分牌监测操作数可用性,可用时通知功能部件从寄存器中读出源操作数开始执行(RAW冲突)
+定时器
+//只执行一次 |
//一次性定时器 |
获取其他BOM对象
+history、location、navigator、screen
写结果
-记分牌监测是否完成执行,若不存在or已消失WAR,则写入;存在,等待
+获取DOM对象
+document
|
刷新
+location.reload方法
性能分析
-设置或返回完整的url
+location.href属性
核心思想
-记录和检测指令相关,操作数一旦就绪立刻执行,把发生RAW的可能减到最小;
-通过寄存器换名消除WAR和WAW(上面的记分牌是通过等待)
-基本结构
-保留站
-每个保留一条已经流出并且等待到本功能部件执行的指令的相关信息。包括操作数、操作码以及各种元数据。
-故而,需要有以下字段:
+
|
web前端框架
+基本模板:
+
|
实现依赖于栅格系统。
+将一行平均分成12个格子,可以指定元素占几个格子
+可以感受到,其实跟我们之前那个纯纯HTML做页面的思想是差不多的,都是把整个页面看做一个表,表有很多行,每行有不同的格子。
+Op:操作
-Qj,Qk:操作数保留站号
-Vj,Vk:源操作数值
-load的Vk保存偏移量
-Busy
-A:存放立即数字段 or 有效地址,仅用于load和store缓冲器
-Qi:寄存器状态表
-存放把结果写入该寄存器的保留站ID
-公共数据总线CDB
-用于发送各个功能部件的计算结果。如果具有多个执行部件且采用多流出流水线,则需要采用多条CDB。
-load缓冲器和store缓冲器
+容器分类:
设备代号:
+浮点寄存器FP
-
|
看文档。
+
|
*{ |
这东西写了我还挺久的。。。不过收获也挺多。
+text-align
+是一个css属性,我觉得挺好用的(。我不知道它精确是什么意思,但我发现它好像有种能让该元素下的子元素水平居中的效果。
指令队列
-FIFO
+关于“容器”的理解
+上面说过,Bootstrap有个容器的概念,跟我们上面纯HTML的表格概念其实是很类似的。
+HTML的容器是表格标签,Bootsrap的容器是container-fluid和container类的标签。
+与HTML的表格相同,“容器”也是可以嵌套的。这点在本案例体现为一下两点:
+① container中可以嵌套container-fluid。
+ 案例中,页首-轮播图和页尾这两段是两边不留白的,轮播图-页尾这段是两边留白的。所以,我们就可以让整体为一个container容器,中间一段再用container-fluid容器包装起来。也即:
+<body> |
注意,此处不要作死为了优雅统一性这样写:
+<div class="row container"></div> |
也即多加一个row类。要不row的属性会覆盖掉container的。
+② 对于“col-md-4”这些的理解
+在做这样的包含row-span元素的行时,之前的解决方案是采用表格嵌套。同样的,这里也可以采用容器嵌套。而此时,列的书写方式就比较特殊了。
+<div class="row"> |
其实是非常直观的,相信以后你看到这段应该也能理解(。提示一点,栅格系统其实好像是相对于父类的。也就是说,不是“把整个页面分成12个格子”,而是,“把父类占有的空间分成12个格子”。
运算部件
-浮点加法器、浮点乘法器
+关于hr标签
+使用css改颜色时应该写background: orange;
而不是color: orange;
。
xml叫做可扩展标签语言。它的全部标签都是自定义的。
+ 1. xml文档的后缀名 .xml
+ 2. xml第一行必须定义为文档声明
+ 3. xml文档中有且仅有一个根标签
+ 4. 属性值必须使用引号(单双都可)引起来
+ 5. 标签必须正确关闭
+ 6. xml标签名称区分大小写
+
寄存器换名实现
-当指令流出,如果操作数缺失,则将指令数据换名为保留站编号
+
|
常见属性:version[必须]、encoding、standalone[取值为yes和no,yes为依赖其他文件]
+id属性值唯一
+这种要转义来转义去的显然很麻烦。所以就需要用到CDATA区。
+CDATA区的文本会被原样展示。
+<code> |
只能写约束文件内的标签
+//students标签里可以包含若干个student标签 |
//外部引入 |
或者可以直接在xml内部写:
+
|
<student number="s001"> |
|
|
它这意识就是,每个schema文件都要起一个别名,比如xsi:schemaLocation="http://www.itcast.cn/xml student.xsd"
这行代码实际上就是把student.xsd
的别名起为了http://www.itcast.cn/xml
。
为什么要起名呢?这就类似于命名空间这种东西,你要用到一个标签需要指明这个标签从哪来的,比如std::vector
,Class.toString
这种。这种命名空间在xml里叫前缀。所以,实际上完整写法应该是<http://www.itcast.cn/xml:students>
。
但这样显然太麻烦了,别名一般都是这种网址,写起来太长了。所以我们选择给别名起别名,设置方法为xmlns:a="http://www.itcast.cn/xml"
,这样一来,以后就不用写<http://www.itcast.cn/xml:students>
,只用写<a:students>
了。
但是如果每个都写一个前缀还是有点难顶。所以就引入了一个空前缀。这样写<students>
这样没有前缀的标签,就相当于从空前缀那个命名空间里拿出来的了。当然如果有多个命名空间,还是得区分一下的。
解析方式有两种方法。
+将标记语言文档一次性加载进内存,形成DOM树
+操作方便,可以进行CRUD所有操作;但占内存
+逐行读取,基于事件驱动
+不占内存;但只能读取
+主要学习Jsoup。
+跟前面html的DOM是差不多的。
+public class JsoupDemo { |
Jsoup:工具类,可以解析html或xml文档,返回Document
+特点
-冲突检测与指令执行是分布的
-通过保留站和CDB实现
-计算结果通过CDB直接从产生它的保留站传送到所有需要它的功能部件,无需经过寄存器
+消除了WAW和WAR
+Document:文档对象。代表内存中的dom树
+执行步骤
-3段流水
-流出
-如果操作要求的保留站空闲(结构冲突),则送到保留站r。如果操作数已就绪,填入;否则,填入产生该操作数的保留站ID(寄存器换名,消除WAW、WAR)。
+Elements:元素Element对象的集合。可以当做 ArrayList
执行
-两个操作数就绪后,就可以用保留站对应功能部件执行
-Element:元素对象
+这一点很好理解。因为Document和Element对象的获取元素方法都继承自Node结点,本意就是获取子元素对象。只不过Document是根节点,所以就变成了获取所有元素对象。
+写结果
-计算完毕后由CDB传送
+Node:节点对象
+使用选择器selector
+其实语法格式跟css的那个选择器差不多。
+/** |
使用XPath
+XPath:xml路径语言。
+ +/** |
Tomcat是Java相关的web服务器软件。
+启动
省流:看系统环境变量有没有CATALINA_HOME这一项,并且看这个CATALINA_HOME的值是否与当前版本安装路径相符合。
+我电脑上本来也有了一个tomcat,只不过跟老师版本不一样。我把这两个都安到同一个目录了。然后我启动了老师版本,却发现输入localhost:8080没有任何响应。我首先去看了一下tomcat的config下的server.xml,发现端口号确实是8080没问题。然后试图访问localhost,发现没有响应,故推测是此处发生了问题。因而我上网按照该教程做了一遍:
+127.0.0.1 拒绝了我们的连接请求–访问本地IP时显示拒绝访问
+我重启电脑后,再次启动老师版本,发现还是不行。这时我开始怀疑是否我的tomcat没有正常启动,或者是否是因为8080这个端口号冲突了。所以我又找了一下如何查看端口号占用情况:
+ +用netstat -a
命令即可。我便发现,在我开着tomcat的情况下,8080这个端口没有被使用。说明好像启动不大正常。于是我打开了另一篇回答:
按照它说的去查看日志文件。发现老师版本的tomcat下的log目录为空。我就去我本安装的版本下的log目录去看了,惊奇地发现,原来我在使用老师版本的tomcat时,tomcat用的是老版本的log目录。也就是说,很有可能config目录也是用的老版本的。我去查看老版本的config,发现端口是8888。于是我把老师版本的tomcat卸载了,去访问localhost:8888,成功力。
+我探寻了以下原因,发现tomcat的startup里面如此写道:
+if not "%CATALINA_HOME%" == "" goto gotHome |
这一段大概是在找到tomcat这个软件的位置。如果我们在环境变量里面设置了CATALINA_HOME,那么就会直接把软件位置定位到CATALINA_HOME的值的地方,随后之后的逻辑都在那边执行。
+我发现我确实设置了这个CATALINA_HOME,并且:
+它的值是我电脑原本有的老版本的目录!
+故而,这也就说明了为什么老师的版本不去用自己的log,不去用自己的config,而用的是我电脑上的老版本的log,config了。。。
+部署项目的方式:
假设每个时钟周期流出两条,1整数型指令+1浮点型指令。
-整数型:load、store、分支
-浮点型:可能各种运算吧
+直接将项目放到webapps目录下即可。 * /hello:项目的访问路径–>虚拟目录 * 简化部署:将项目打成一个war包,再将war包放置到webapps目录下。
+假设所有浮点指令都是加法,执行时间3个时钟周期,且图中整数总在浮点前
+配置conf/server.xml文件
在<Host>
标签体中配置
<Context docBase="C:\aWorkSpace\Projects\Java\JavaWeb" path="/web" />
没懂,难道单发射流水线就不会吗。。。
-后面不知道为什么写着写着开始英文了。。。算了,看起来都不重要。
-GPU中的线程是执行计算任务的最小单位,可以看作是一系列指令的执行者。每个线程都有自己的程序计数器(PC)、寄存器集和局部内存。这些线程以并行的方式执行相同的指令,但可以有不同的输入数据,从而在数据并行的模式下执行计算。
-下面两个标题反了额
-感觉能明白其划分一组组线程的意义了,就是方便管理,一个warp执行相同的指令代码,所以要求同时调度同时执行
-真没懂。。。。
-真没看懂
-TODO接下来有兴趣看吧
-相比于fall2022(Trie),spring2023(COW-Trie)的难度更大,算法更复杂【毕竟是要实现一个cow的数据结构】,我认为两个都很有意义,故而两个都做了。
-其中在Trie中,由于我是第一次接触cpp,所以遇到了很多麻烦。好在经过18h+的cpp拷打后,cow-trie虽然难度大,语法也更复杂一些,但我还是很快(话虽如此也花了7、8小时23333)就完美pass了。不过效率可能还是不大高,毕竟我不熟悉cpp,很多地方可能都直接拷贝了emm希望后续学习可以加把劲。
---In this project, you will implement a key-value store backed by a concurrent trie. 实现并发安全的trie
-To simplify the explaination, we will assume tha the keys are all non-empty variable-length strings but in practice they can be any arbitrary type. key为非空变长字符串
-The key-value store you will implement can store string keys mapped to values of any type. key必须是字符串类型,但value可以是任意类型
-The value of a key is stored in the node representing the last character of that key.
--
本次实验完成时间总计18h+。是的,lab0就做了这么久【难绷】
-其实光就实验内容来看,无非就是实现trie树,算法上没有很难,最难的应该是Remove函数的编写,因为它是个递归。
-但正如本次实验的主题C++ Primer
所揭示的那样,本次实验的真正难点在于C++……而在接触本实验之前,我对c++一无所知。
除了这个萌新debuf之外,我还不小心犯了另一件非常sb的乌龙,加上对cpp实在是太小白了,再加上这几天破事又贼多,更是让我心态大崩,差点一蹶不振不想写了(。
-因而,整个实验在我看来十分痛苦。coding阶段,就是 语法错误-看了半天报错信息才发现哪错了-改错误-改得不对-再改-再改-再改……这样的痛苦过程在循环往复;运行阶段,就是看着stack trace发呆、用gdb调来调去还不知道为什么错了这样的痛苦过程在循环往复。好在,我还是坚持下来了,虽然内心还是很浮躁很浮躁(
-不过总而言之,我认为这次实验给我收获挺大的。它帮助我熟悉了C++,但我认为更重要的,是它帮我矫正了心态。做这个实验之前,我内心是很浮躁的(那会破事太多了),而且因为它是lab0所以有点轻敌(对不起。。),因而我所采取的策略是“错误驱动”,也即哪里报错就百度下怎么改就行。这样的心态就导致我的debug过程极度痛苦,因为完全看不懂报错信息,压根不知道错在哪里,百度也百度不出来。于是我被迫修改了战略,去看了我一直不想看的书,学了我一直很害怕的cpp,用了我一直很抗拒的gdb调试,才发现其实都没有我想象的这么恐怖。这期间、这几天的种种心路历程,我认为是十分可贵的。
-我下载下来starter code的时候,发现找不到它要我们实现的p0_trie.h
,只有这几个:
我便觉得可能是实验代码改版了。但是我并没有多想,我觉得可能只是代码模板改版了但实验内容不变QAQ【为什么会这么觉得呢?因为我看到指导书的url为fall2022便以为这是最新版指导书,没有想到春季学期也可以开课,还有个spring2023呃呃】而且代码看起来也确实是要我们实现Trie树【虽然跟指导书说得不大一样】。故而,我就这么直接开干了。
-写完了Tire树的逻辑【这部分确实挺简单的】之后,我就开始了漫长的痛苦且折磨的原地兜圈之旅。由于真正的spring2023的代码模板是实现COW-Trie,故而代码模板中很多地方都使用了const关键字,包括树结点以及树的children_成员。
-// in class Trie |
如上是spring2023的代码模板。
-如果使用其给我们提供的COW-Tire接口来实现Trie树,就会产生巨大的矛盾。你无法在root_
的孩子中插入或者删除一个树节点,因为root_
指向一个const对象,其children_
域也是const的。同样的,你也无法对root_
的孩子的孩子集合做增删结点操作,因为它也是const的。
由于对C++不熟悉,通过满屏幕的报错从而搞清楚上面那些东西这件事,就花费了我很多很多时间。
-error: no matching function for call to |
比如说这个错误我就看了半天完全不知道啥意思(
-好在明白上面这点后,我很快就发现了spring2023的存在,然后切到了fall2022的正确分支【乐】
-经过了此乌龙后,我深刻地意识到了我对C++一窍不通的程度(,比如说上面的这些const,还有比如说&是什么东西&&又是什么东西,shared_ptr又是什么东西等等等,我都不懂。故而,我压制了内心的浮躁,去简单看了一下书,了解了new的作用、左值引用右值引用、move、智能指针这几个地方,然后再去重新开始写本实验,最终果然好了不少。
-在Trie::GetValue
中,我本来是这么写的:
std::unique_ptr<TrieNode>* t = &root_; |
这就会导致,tmp和(*t)会指向同一块内存区域,并且它们都是unique_ptr
。随后,代码块遇到}
结束,tmp的析构函数被调用,那块内存区域被free,但(*t)依然指向那块内存区域,随后在释放整个Trie树时这块区域就会被再次释放,然后寄(
有一个方法可以在不剥夺某个unique_ptr
的所有权的同时,又能用另一个变量来操作该指针所指向的对象。这个方法就是——使用指向unique_ptr
的指针(。
也即比如:std::unique_ptr<TrieNode> *
本次实验还格外要求了代码规范问题。
-make format |
然后之后访问时输入`localhost/web/JavaWeb.html`即可
-自测
我暂时没进行gradescope的自测,原因是它上面报了个跟我没啥关系的错,我不知道怎么改呃呃。
-![image-20230318165521340](/2023/03/13/cmu15445/image-20230318165521340.png)
-In file included from /autograder/bustub/src/common/bustub_instance.cpp:17:
/autograder/bustub/src/include/common/bustub_instance.h:30:10: fatal error: 'libfort/lib/fort.hpp' file not found
include "libfort/lib/fort.hpp"
+* docBase:项目存放的路径
+* path:虚拟目录
+
+<Context docBase="C:\aWorkSpace\Projects\Java\JavaWeb" />
注意,该方法是热部署的。也就是说,可以不关闭服务器的情况下,去增删xml文件,会马上变化,而不是像上面两种方式一样重启生效。
+
+
+
+项目都存放在webapp里。打开webapp中的任一个。
+WEB-INF下是动态资源,也就是Java控制的一些文件【大概这个意思】。有这个文件夹的项目是动态项目。
+WEB-INF以外的都是静态资源。
+然后等着它开始下载就行了。
+最后的目录结构:
+如果java或者resources目录没有,自己建就行。
+1.
+ +2.
+还有另一种更便捷的方式,就是直接添加maven的tomcat插件。在pom.xml文件里加入此段:
+<build> |
都指向说找不到这个fort。但我真的不知道它为啥找不到,因为我看CMakeLists.txt中已经加了third_party/
这个include目录了,并且这个东西的路径也确实是third_party/libfort/lib/for.hpp
。
我还在CMackLists.txt
、src/CMackLists.txt
、tools/shell/CMackLists.txt
里面都加了include(${PROJECT_SOURCE_DIR}/third_party/libfort/lib/fort.hpp)
,但是依然报了这样的错:
它这为啥找不到我是真的很不理解。
-所以真的很奇怪。暂且先放着吧,之后有精力研究下这些编译链接过程。
--CMU 15445 Project 0 (Spring 2023) 学习记录 参考了task2和一个bug
+即可,可用alt+insert自动补全。
+这里我出现了一个飘红报错问题,用这个可以解决:
+maven学习 & Plugin ‘org.apache.tomcat.maven:tomcat7-maven-plugin:2.2’ not found报错解决【问题及解决过程记录】
+然后,右键项目就可以run了:
++
如果没有此选项,就去下载maven helper插件。
+修改tomcat配置参数
图形化界面
run-edit configuration-tomcat
+配置文件
+
启动服务器时控制台前几句输出有一句这样的。对应目录下的就可以找到tomcat配置文件。
+Servlet
server applet运行在服务器端的小程序
+servlet是java编写的服务器端的程序,运行在web服务器中。作用:接收用户端发来的请求,调用其他java程序来处理请求,将处理结果返回到服务器中
++
servlet是接口,定义了Java类被tomcat执行、被浏览器访问的规则。
++
快速入门
+
这里的配置用的是注解,具体原理在第一部分的JavaSE基础里有详细描述了。
++-使用maven创建web项目见上面的tomcat-tomcat集成到IDEA-使用maven创建web项目
先放个通关截图~
--
总体用时(coding+debug+note)10h+
-本次实验是在它给的接口的基础上,实现一株并发安全的cow的trie树,还有一个小小的实现
-upper
和lower
函数的实验用来熟悉我们之后要写的db的东西。算法难度还是有一些的,我的coding和debug时间估摸着可能有46开。总体来说整个实验还是非常有价值的,相比往年难度和意义都更上了一层。感谢实验设计者让我做到设计得这么好的实验~
-Task1 cow-trie
--In this task, you will need to modify
-trie.h
andtrie.cpp
to implement a copy-on-write trie.下面举例说明
-Consider inserting
-("ad", 2)
in the above example. We create a newNode2
by reusing two of the child nodes from the original tree, and creating a new value node 2. (See figure below)-
If we then insert
-("b", 3)
, we will create a new root, a new node and reuse the previous nodes. In this way, we can get the content of the trie before and after each insertion operation. As long as we have the root object (Trie
class), we can access the data inside the trie at that time. (See figure below)-
One more example: if we then insert
-("a", "abc")
and remove("ab", 1)
, we can get the below trie. Note that parent nodes can have values, and you will need to purge all unnecessary nodes after removal.-
To create a new node, you should use the
-Clone
function on theTrieNode
class. To reuse an existing node in the new trie, you can copystd::shared_ptr<TrieNode>
: copying a shared pointer doesn’t copy the underlying data.You should not manually allocate memory by using
+new
anddelete
in this project.std::shared_ptr
will deallocate the object when no one has a reference to the underlying object.+-如果已经导入依赖坐标却还未生效,就点击右侧侧边栏的maven刷新。
+感想
task1的目标就是实现我们的cow-trie的主体,先不要求并发。
-虽说算法上比较复杂,但是由于它图解以及代码中的注释解说都已经说得很详细了,再加上之前已经写过了trie树有一个大体框架,因而具体coding的时候思路还是比较清晰的。
-我认为具体的难点还是在于cpp上。下面列出了几个比较有价值的错误和相关debug过程,其中
-const转移
和显示保存is_value_node_
是我认为两个比较难的点。错误集锦
const转移
在
-trie.h
中:- -
class Trie {
private:
// The root of the trie.
std::shared_ptr<const TrieNode> root_{nullptr};
// Create a new trie with the given root.
explicit Trie(std::shared_ptr<const TrieNode> root) : root_(std::move(root)) {}
public:
// Create an empty trie.
Trie() = default;
// Get the value associated with the given key.
// 1. If the key is not in the trie, return nullptr.
// 2. If the key is in the trie but the type is mismatched, return nullptr.
// 3. Otherwise, return the value.
template <class T>
auto Get(std::string_view key) const -> const T *;
// Put a new key-value pair into the trie. If the key already exists, overwrite the value.
// Returns the new trie.
template <class T>
auto Put(std::string_view key, T value) const -> Trie;
// Remove the key from the trie. If the key does not exist, return the original trie.
// Otherwise, returns the new trie.
auto Remove(std::string_view key) const -> Trie;
};可以看到,为了呼应我们的cow-trie,在语法上强制性要求不能“directly modify”,它将
-root_
和children_->second
同时设置为了一个指向对象为const的指针。而这意味着什么呢?意味着我们不能修改root_
的内容,也不能修改root_->children_->second
的内容,同样的孩子的孩子也不行。这就需要我们在Put
方法中遍历trie时,对遍历路径上的每个结点都需要copy一次,故而我们的代码具体是如下实现的:首先,利用
-TrieNode::Clone()
方法来创造一个非const指针的新root:- -
// in trie.h TrieNode{}
virtual auto Clone() const -> std::unique_ptr<TrieNode> { return std::make_unique<TrieNode>(children_); }+
// 创造新的根节点,并且为非const类型
std::shared_ptr<TrieNode> root = std::shared_ptr<TrieNode>(root_->Clone());
// 使用t指针来遍历trie树
std::shared_ptr<TrieNode> t = root;原理
执行原理
+
+- 执行原理:
++
+- 当服务器接受到客户端浏览器的请求后,会解析请求URL路径,获取访问的Servlet的资源路径
+- 查找web.xml文件,是否有对应的
+标签体内容。 - 如果有,则在找到对应的
+全类名【注意:在下面,url-pattern都使用注解配置方法了,所以这两步应该是不用了,应该会变成这样:① 逐个遍历注册的servlet实现类,查看其注解属性是否为对应的url-pattern。② 如果有,则找到类名,步骤继续】 - tomcat会将字节码文件加载进内存,并且创建其对象
+- 调用其方法
+生命周期
+
并发安全
Servlet的init方法只执行一次,一种Servlet在内存中只存在一个对象,Servlet是单例的。因而,当多线程同时访问同一个Servlet对象时,就会产生线程安全问题。所以有需要的话,就要采取手段保障Servlet类的线程安全性。
+体系结构
为了简化开发,我们可以用提供的servlet的实现类。
++
GenericServlet
除了service方法之外的方法,差不多都只做了空实现。所以只需写service方法即可。
+-
public class Servletdemo2 extends GenericServlet {
public void service(ServletRequest servletRequest, ServletResponse servletResponse){
}
}再然后,每次迭代的时候在遍历路径上创造新的结点,结点类型非const;再利用shared_ptr的共享复制(
-t = tmp;
),就能使得当前的t指针一直保持非const状态。+
for (uint64_t i = 0; i < key.length(); i++) {
auto it = t->children_.find(key.at(i));
if (it == t->children_.end()) {
if (i != key.length() - 1) {
std::shared_ptr<TrieNode> tmp(new TrieNode());
// ...
t = tmp;
} else {
std::shared_ptr<TrieNodeWithValue<T>> tmp(new TrieNodeWithValue(std::make_shared<T>(std::move(value))));
// ...
t = tmp;
break;
}
} else {
if (i == key.length() - 1) {
std::shared_ptr<TrieNodeWithValue<T>> node =
std::make_shared<TrieNodeWithValue<T>>(it->second->children_, std::make_shared<T>(std::move(value)));
// ...
t = node;
break;
}
std::shared_ptr<TrieNode> node = std::shared_ptr<TrieNode>(it->second->Clone());
// ...
t = node;
}
}HttpServlet
使用
比如httpservlet,就只用重写里面的doGet和doPost两个方法就行。
+-
public class Servletdemo2 extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp){
System.out.println("get!!!");
}
protected void doPost(HttpServletRequest req, HttpServletResponse resp){
System.out.println("post!!!");
}
}-+注:我本来的写法是这样的:
-+
for (uint64_t i = 0; i < key.length(); i++) {
auto it = t->children_.find(key.at(i));
if (it == t->children_.end()) {
if (i != key.length() - 1) {
std::shared_ptr<TrieNode> tmp(new TrieNode());
// ...
} else {
std::shared_ptr<TrieNodeWithValue<T>> tmp(new TrieNodeWithValue(std::make_shared<T>(std::move(value))));
// ...
break;
}
} else {
if (i == key.length() - 1) {
std::shared_ptr<TrieNodeWithValue<T>> node =
std::make_shared<TrieNodeWithValue<T>>(it->second->children_, std::make_shared<T>(std::move(value)));
// ...
break;
}
std::shared_ptr<TrieNode> node = std::shared_ptr<TrieNode>(it->second->Clone());
// ...
}
it = t->children_.find(key.at(i));
t = it->second;
}这两个方法的区别就是,当使用get方式提交表单,就会执行第一个方法;使用post则会执行第二个方法。
+比方说post时:
+网页代码如下(放在webapp目录下)
+-
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
Hello,World!
<!-- action内写Servlet的资源路径 -->
<form action="/webdemo4_war/demo2" method="post">
name: <input type="text" name="username" id="username" placeholder="请输入用户名">
<input type="submit" value="submit">
</form>
</body>
</html>也就是为了省事,将t指针的转移集中放在了循环体最后进行。但这样是不行的。
-cpp中,可以将非const对象自然转移为const对象,比如代码中就将非const的新结点放进了
-children_
中;但是不允许将const对象自然转移为非const对象,比如代码中的t = it->second;
。因而,我们对t指针的转移不能在新结点放入其children_
之后。servlet代码同上。
+最终在网页中点击提交
++
会跳转到\demo页面【也即servlet的访问路径】,并且在console打印“post!!!”
--注2:在这里,我本来还多用了一个prev指针,因为在coding的时候用的是上面的本来的写法,误以为t指针只能是const,所以还得有父节点才能再把t指针复制一遍。但其实并非如此,而且就算如此prev指针也还是跟t指针一样的const的。。。不过还好编译前发现了上面那点改过来了,要不然就得面对编译大报错2333
+为啥会这样呢?
+之前在讲表单的时候说过,form的action属性代表着提交时这个表单会提交给谁,值为一个URL。所以,这里action的值设置为Servlet的路径,意思就是把表单数据发送给了Servelet,由于使用的是post方式,因此触发了Servlet的doPost方法。Servlet对得到的数据进行各种处理,并且通过req和resp进行交互。
make_shared
make_shared
作用也类似于new,会在堆上开辟空间用以存放共享指针的base对象。这也让我想起来我在做上面那个实验时一个地方改成make_shared
就对了,估计是犯了用栈中对象创建共享指针的错误。--官方鼓励用
+make_shared
函数来创建对象,而不要手动去new。这一是因为,new出来的类型是原始指针,make_shared
可以防止我们去使用原始指针创建多个引用计数体系;二是因为,make_shared
可以防止内存碎片化。为什么此处写的是“\demo”这样的路径?
+事实上这是一个相对路径。
++
部署的根路径可以在 run-edit configuration-tomcat-deployment中找到。
一个奇妙的报错
在写这样的shared_ptr的共享转移时:
-- -
std::shared_ptr<TrieNode> tmp = make_shared<TrieNode>();
// ...
t = tmp;会在
-t=tmp
这里报错不能把int类型的tmp复制给t。我看了半天很奇怪哪来的int类型,查了半天怎么共享shared_ptr,最后才发现是因为这里:- -
std::make_shared<TrieNode>()漏了个
-std::
呃呃。显式保存is_value_node_
在
-trie.cpp RemoveHelper()
中:- -
if (i != key.length() - 1) {
// 注意此处需要保留原来的is_value_node_,之后再赋值回去!!!
bool tmp_val = node->second->is_value_node_;
std::shared_ptr<TrieNode> tmp = std::shared_ptr<TrieNode>(node->second->Clone());
tmp->is_value_node_ = tmp_val;
root->children_.erase(key.at(i));
root->children_.insert(std::make_pair(key.at(i), tmp));
flag = RemoveHelper(tmp, key, i + 1);
}否则会:
--
查看
-trie_test.cpp
的代码:+
TEST(TrieTest, BasicRemoveTest2) {
auto trie = Trie();
// Put something
trie = trie.Put<uint32_t>("test", 2333);
ASSERT_EQ(*trie.Get<uint32_t>("test"), 2333);
trie = trie.Put<uint32_t>("te", 23);
ASSERT_EQ(*trie.Get<uint32_t>("te"), 23);
trie = trie.Put<uint32_t>("tes", 233);
ASSERT_EQ(*trie.Get<uint32_t>("tes"), 233);
// Delete something
trie = trie.Remove("te");
trie = trie.Remove("tes");
trie = trie.Remove("test");
ASSERT_EQ(trie.Get<uint32_t>("te"), nullptr);
ASSERT_EQ(trie.Get<uint32_t>("tes"), nullptr);
ASSERT_EQ(trie.Get<uint32_t>("test"), nullptr);
}深层一些的问题
分成get和post
之所以这两种方法需要分别处理,是因为在Servlet的service方法中,其实是要对req对象进行参数分解,这两种方法分解方式不一样。
+按照以往,我们需要这样写
+-
public void service(ServletRequest servletRequest, ServletResponse servletResponse) {
String method = ((HttpServletRequest)servletRequest).getMethod();
if("GET".equals(method)){
//执行get的逻辑
}
else if ("POST".equals(method)){
//执行post的逻辑
}
}它是在
-ASSERT_EQ(trie.Get<uint32_t>("te"), nullptr);
这句报错的。这确实很奇怪,因为“te”已经被remove了。这是为什么呢?经过gdb调试,trie的Remove和Put功能都确实很正常,但是我发现了一个诡异的现象。
-在经过
-trie = trie.Remove("te");
这句话后,trie的状态是t-e-(s)-(t)【括号表示为有值结点,类型为TireNodeWithValue
】,符合预期。但是,经过紧随其后的trie = trie.Remove("tes");
之后,trie的状态却变成了t-(e)-s-(t)。-
这实在是很诡异,为什么经过了一次Remove之后,
-trie = trie.Remove("te");
这句话的效果就被重置了?我想了挺久,最终认为这是构造方法的问题。
-再次看一遍我们的Remove的代码:
-+
if (i != key.length() - 1) {
std::shared_ptr<TrieNode> tmp = std::shared_ptr<TrieNode>(node->second->Clone());
root->children_.erase(key.at(i));
root->children_.insert(std::make_pair(key.at(i), tmp));
flag = RemoveHelper(tmp, key, i + 1);
} else {
if (node->second->is_value_node_) {
if (!node->second->children_.empty()) {
std::shared_ptr<TrieNode> tmp = std::shared_ptr<TrieNode>(node->second->Clone());
tmp->is_value_node_ = false;
root->children_.erase(key.at(i));
root->children_.insert(std::make_pair(key.at(i), tmp));
} else {
root->children_.erase(key.at(i));
}
return true;
}
return false;
}就类似于可以这么写:
+-
public void service(ServletRequest servletRequest, ServletResponse servletResponse){
String method = ((HttpServletRequest)servletRequest).getMethod();
if("GET".equals(method)){
doGet(servletRequest,servletResponse);
}
else if ("POST".equals(method)){
doPost(servletRequest,servletResponse);
}
}以及
-TrieNodeWithValue::Clone()
:+
auto Clone() const -> std::unique_ptr<TrieNode> override {
return std::make_unique<TrieNodeWithValue<T>>(children_, value_);
}于是最后就融合入httpservlet了。
+url-pattern配置
+
- +
一个Servlet可以定义多个访问路径 : @WebServlet({“/d4”,”/dd4”,”/ddd4”})
+路径定义规则:
++
-- /xxx:路径匹配如/demo、/*【第一个优先级大于第二个】
+- /xxx/xxx:多层路径,目录结构
+- *.do:扩展名匹配不能在前面加’/‘。也即:
-
以及Clone()方法调用的
-TrieNodeWithValue
的构造方法:- -
explicit TrieNodeWithValue(std::shared_ptr<T> value) : value_(std::move(value)) { this->is_value_node_ = true; }可以发现,在多态作用下,e结点始终是一个
-TrieNodeWithValue
的结点。在我们去除tes这个key时,会到这个分支:
-- -
if (i != key.length() - 1) {
std::shared_ptr<TrieNode> tmp = std::shared_ptr<TrieNode>(node->second->Clone());Clone()中会调用
-node->second
,也即e结点的构造方法,然后将e结点的is_value_node_
设置为true,从而导致Get
中无法通过这句代码返回nullptr。- -
if (!(t->is_value_node_)) {
return nullptr;
}因而,为了解决这个问题,我们就需要暂存
-is_value_node_
,并在之后恢复它。Task2 concurrency
--In this task, you will need to modify
-trie_store.h
andtrie_store.cpp
.需要实现并发安全版本。For the original Trie class, everytime we modify the trie, we need to get the new root to access the new content. But for the concurrent key-value store, the
-put
anddelete
methods do not have a return value. This requires you to use concurrency primitives to synchronize reads and writes so that no data is lost through the process.在并发安全版本中,Put
和Get
不会返回trie,而是应该修改包装类的base trie。Your concurrent key-value store should concurrently serve multiple readers and a single writer. That is to say, when someone is modifying the trie, reads can still be performed on the old root. When someone is reading, writes can still be performed without waiting for reads.同一时刻可以有一个writer和多个reader。
-Also, if we get a reference to a value from the trie, we should be able to access it no matter how we modify the trie. The
-Get
function fromTrie
only returns a pointer. If the trie node storing this value has been removed, the pointer will be dangling. Therefore, inTrieStore
, we return aValueGuard
which stores both a reference to the value and the TrieNode corresponding to the root of the trie structure, so that the value can be accessed as we store theValueGuard
.为我们提供了ValueGuard
用以确保return值长时间有效。To achieve this, we have provided you with the pseudo code for
-TrieStore::Get
intrie_store.cpp
. Please read it carefully and think of how to implementTrieStore::Put
andTrieStore::Remove
.我们在Get
方法中给出了详细的步骤引导。你需要依据它来对Put
和Get
进行修改。感想
task2的内容是实现cow-trie并发安全版本的包装类
-TrieStore
。相比于fall2022的并发内容,由于加上了cow的特性,本次实验更加复杂。我写了三版都没写对,看到别人的才豁然开朗(很遗憾没有自己再多想会儿……)接下来就从我的错误版本开始,逐步过渡到正确版本吧。
-错误集锦
版本1
-
Get
的实现很简单,按他说的一步步做就行,在这边不做赘述。Put
和Remove
思路差不多,在此只放Put
的代码。-
这样看起来很合理:同一时刻似乎确实只有一个writer对root_进行修改,也似乎确实同时可以有别的线程获取root_lock_对其进行读取。但其实,前者是错误的。
-假如说进程A和进程B都在
-Put
逻辑中。进程A执行到了root_ = new_trie
这句话,然后进程B进入到root_.Put
中。-
root_ = new_trie
使用了运算符=的默认实现,进行浅拷贝,故而会修改root_->root_
;root_.Put
中会对root_->root_
进行移动。进程B在Put中执行
-std::move(root_)
之后,进程A又让root_->root_
变成了别的值(trie浅拷贝),导致原来的root_的引用计数变为0,自动释放(因为是智能指针shared_ptr),进程B在Put中再次访问就会寄。--注,此处是因为智能指针引用计数为零才释放的,cpp没有垃圾回收机制。
-版本2
- -
void TrieStore::Put(std::string_view key, T value) {
// You will need to ensure there is only one writer at a time. Think of how you can achieve this.
// The logic should be somehow similar to `TrieStore::Get`.
root_lock_.lock();
Trie tmp = root_;
root_lock_.unlock();
write_lock_.lock();
Trie new_trie = tmp.Put(key, std::move(value));
write_lock_.unlock();
root_lock_.lock();
root_ = new_trie;
root_lock_.unlock();
}版本1错误后,我发现我并没有按它强调的“somehow similar to Get”那样,模仿
-Get
中的写法来做。于是我就修改了下,版本2诞生了。但是这样的话,依然不能解决版本1中的问题。所以我又搞了个版本3.
-版本3
- -
void TrieStore::Put(std::string_view key, T value) {
write_lock_.lock();
Trie tmp = root_;
Trie new_trie = tmp.Put(key, std::move(value));
root_ = new_trie;
write_lock_.unlock();
}这样就能通过所有测试了。
-但这样做虽然能解决多个writer的争夺问题,但不能解决一个writer和一个reader的争夺问题:因为两者都争夺同一个root_变量,但只有reader争夺root_lock_,这显然很不安全。因而,终极版本应该是这样:
-正确版本
- - - -
template <class T>
void TrieStore::Put(std::string_view key, T value) {
write_lock_.lock();
root_lock_.lock();
Trie tmp = root_;
root_lock_.unlock();
Trie new_trie = tmp.Put(key, std::move(value));
root_lock_.lock();
root_ = new_trie;
root_lock_.unlock();
write_lock_.unlock();
}可以看到整个思维过程是线性的,逐步改进下来,正确答案其实很容易想到。只可惜我太浮躁了,没有静下心来好好想,在版本3之后就去看了眼别人怎么写的(罪过)没有独立思考,算是一个小遗憾。
-Task3 debugging
感想
一个考查我们debug入门技巧的小任务,简单,但我觉得形势很新颖。
-debug过程
随便贴点debug过程的截图。
--
-
-
小问题
gdb:Attempt to take address of value not located in memory.
任务中,需要获取root_的孙子。所以我就这么写了个gdb指令:
-p root_->children_.find('9')->second
,然后就爆出了标题这个错误。百度了下看到了这个:
-- ---
也许是因为我们通过.访问了children_的成员find吧(
-总之,我最后是在
-trie_debug_test
添加了这几行代码解决的:- -
// Put a breakpoint here.
// (1) How many children nodes are there on the root?
// Replace `CASE_1_YOUR_ANSWER` in `trie_answer.h` with the correct answer.
if (CASE_1_YOUR_ANSWER != Case1CorrectAnswer()) {
ASSERT_TRUE(false);
}
auto it = trie.root_->children_.find('9');
// (2) How many children nodes are there on the node of prefix `9`?
// Replace `CASE_2_YOUR_ANSWER` in `trie_answer.h` with the correct answer.
if (CASE_2_YOUR_ANSWER != Case2CorrectAnswer()) {
ASSERT_TRUE(false);
}
auto val = trie.Get<uint32_t>("93");
std::cout << val << it->first << std::endl;
// (3) What's the value for `93`?
// Replace `CASE_3_YOUR_ANSWER` in `trie_answer.h` with the correct answer.
if (CASE_3_YOUR_ANSWER != Case3CorrectAnswer()) {
ASSERT_TRUE(false);
}也即添加了it和val,以及防止unused报错的cout语句。gdb调试时打印it和val就行。
-答案对但是过不了评测
--来自CMU 15445 Project 0 (Spring 2023) 学习记录
-在我本地的环境上,调试三问的答案分别是
-8 1 42
,但该答案无法通过 Grade 平台的评测。发现在 Discord 上有人提出了同样的问题,助教 Alex Chi 给出了解答:--Alex Chi — 2023/02/15 23:29
-
It is possible that your environment produces different random numbers than the grading environment. In case your environment is producing different set of random numbers than our grader, replace your TrieDebugger test with:-
auto trie = Trie();
trie = trie.Put<uint32_t>("65", 25);
trie = trie.Put<uint32_t>("61", 65);
trie = trie.Put<uint32_t>("82", 84);
trie = trie.Put<uint32_t>("2", 42);
trie = trie.Put<uint32_t>("16", 67);
trie = trie.Put<uint32_t>("94", 53);
trie = trie.Put<uint32_t>("20", 35);
trie = trie.Put<uint32_t>("3", 57);
trie = trie.Put<uint32_t>("93", 30);
trie = trie.Put<uint32_t>("75", 29);难绷,我反复确认了好几遍(。主要还是太相信cmu的权威了,觉得这实验都发布了好几个月了应该不会有错,就没想到是这个问题。我觉得最好还是把这个问题反应在指导书上吧。
-Task4 SQL String Functions
-Now it is time to dive into BusTub itself!
-You will need to implement
-upper
andlower
SQL functions.This can be done in 2 steps:
--
- implement the function logic in
-string_expression.h
.- register the function in BusTub, so that the SQL framework can call your function when the user executes a SQL, in
+url访问填写plan_func_call.cpp
.http://localhost/webdemo4_war/*.do
。To test your implementation, you can use
-bustub-shell
:-
cd build
make -j`nproc` shell
./bin/bustub-shell
select upper('AbCd'), lower('AbCd');
ABCD abcd感想
说实话乍一看我还没看懂(。它放在这个位置,我还以为跟上面实现的cow-trie有什么关系,并且误以为这个upper和lower是什么上层接口底层接口的意思,跟它大眼瞪小眼了半天。直到看到了下面的案例,才发现跟trie似乎没有任何关系23333
-本次实验内容其实就是实现sql的转换大小写的函数。知道了要做什么之后,任务就很简单了,按着它提示一步步做就行。
-不过此task重点其实也是在稍微了解下我们接下来要打交道的sql框架的代码。比如说,此次我们的实现涉及到的,居然是一个差不多是工厂模式(其实更像策略模式?)的一部分:
-外界传入想调用的函数名,通过
-GetFuncCallFromFactory
获取对应的处理对象-
得到处理对象后调用其
-Compute
方法就行-
第一次如此鲜明地看到一个设计模式在cpp的应用,真是让我非常震撼。
-代码规范
依旧是这三件套:
-- -
make format
make check-lint
make check-clang-tidy-p0错误集锦
关于sanitizer
执行了该命令:
-cmake -DCMAKE_BUILD_TYPE=Debug -DBUSTUB_SANITIZER= ..
之后,执行make
报错missing argument to '-fsanitize='
发生这个的原因是cmake的命令中将
-]]>BUSTUB_SANITIZER
设置成了空。解决方法就是将其设置为别的值就好了,具体想设置成什么值可以参见:关于GCC/LLVM编译器中的sanitize选项用处用法详解 我这里姑且随便设置了个leak
。
先放个通关记录~
---特别鸣谢:
-某不愿透露姓名的友人hhj
- - -
--During the semester, you will build a disk-oriented storage manager for the BusTub DBMS.
-注:DBMS(Database Management System),比如说Oracle数据库
-The first programming project is to implement a buffer pool in your storage manager.The buffer pool is responsible for moving physical pages back and forth from main memory to disk.也就是负责物理页从磁盘的读写
-It allows a DBMS to support databases that are larger than the amount of memory available to the system. 是的,这其实就跟内存换入换出差不多。我们现在是不是要在用户态实现这个功能?我记得xv6似乎是没有这个机制的。有点小期待呀。不过这部分感觉说不定和xv6的磁盘管理(使用双向链表管理buffer cache),及其的改进版本(lab:locking 使用哈希+双向链表)比较类似。
---注:xv6确实没有内存换入换出机制,其是固定大小的内存空间。但xv6的文件系统有采用LRU算法的buffer cache(怪不得有什么数据库型的文件系统,这两个确实有点像)。
-The buffer pool’s operations are transparent to other parts in the system. For example, the system asks the buffer pool for a page using its unique identifier (
-page_id_t
) and it does not know whether that page is already in memory or whether the system has to retrieve it from disk.Your implementation will need to be thread-safe.
-
由于这几天时间比较零散+事情比较多,因此完成的总时间数不一定值得参考:26h(乐)
-本次实验要说简单也还算简单。大概就是实现一个database与磁盘交换页的buffer pool,机制类似于内存换入换出。而实现这个buffer pool,首先得实现换入换出算法,也即task1的LRU-K算法。再然后就是在我们的LRU-K算法的基础上,实现真正的buffer pool(真正指:真正地存储以及读写磁盘、向外暴露接口),也即BufferPoolManager
。最后,我们会实现类似于lock_guard
这样结构的PageGuard
,用于自动释放页引用和读写锁。最后的最后,我们会对实现的buffer pool进行性能优化,优化方向包括细粒度化锁以实现并行IO、针对特定应用场景调整LRU-K策略等。
这四者都是相互联系相互递进的,我认为每一个task都设计得非常不错,写完了之后对它所涉及的知识点都有了更深刻的理解。我认为其中最优美的一点就是LRU-K算法与buffer pool的解耦,这个设计让我十分地赞叹。
-最后,再对我的完成情况进行一个评价。本次实验确实内容不是很难【除了性能调优部分,这个我是真不懂QAQ】,毕竟它指导书以及代码注释都给了详细的步骤参考,我之所以做了那么久一是因为我有不好的习惯,就是没认真看指导书和提示就开始按着自己的理解写,然后写完就直接开始debug开始交了;二是因为这几天学业的破事太多、竞赛也逐步开始了,因而战线拉得太长,总耗时就太多了。
-因而,吸取经验,我之后coding完了之后,再照着指导书仔仔细细地过一遍自己的代码。同时,15445这个实验我也决定先暂时搁置,毕竟接下来这两个月应该会在竞赛和学业两头转,实在不能抽出很大段时间继续写了。
-就酱。
---This component is responsible for tracking page usage in the buffer pool.
-The LRU-K algorithm evicts a frame whose backward k-distance is maximum of all frames in the replacer. LRU-K 算法驱逐一个帧,其backward k-distance是替换器中所有帧的最大值。
-Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access.
-backward k-distance
=现在的时间戳 - 之前第k次访问时的时间戳A frame with fewer than k historical accesses is given +inf as its backward k-distance. 不足k次访问的帧的backward k-distance应该设置为inf(对应上图左边那个访问记录队列吧)
-**When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**如果有多个inf的结点,按照LRU规则淘汰(也即上图左边那个历史记录队列采取LRU规则)
-The maximum size for the
+ + +LRUKReplacer
is the same as the size of the buffer pool since it contains placeholders for all of the frames in theBufferPoolManager
. However, at any given moment, not all the frames in the replacer are considered to be evictable. The size ofLRUKReplacer
is represented by the number of evictable frames. TheLRUKReplacer
is initialized to have no frames in it. Then, only when a frame is marked as evictable, replacer’s size will increase. size为可驱逐的frame数而非所有frame数。service参数
+
http协议
概述
概念:Hyper Text Transfer Protocol 超文本传输协议
++
+- +
传输协议:定义了客户端和服务器端通信时发送数据的格式
+- +
特点:
++
+- 基于TCP/IP的高级协议需要先经历三次握手,可靠传输
+- 默认端口号:80
++-如果说域名是ip地址的简化表示,ip地址又表示着一台主机,那么使用http协议访问一个网址,相当于访问一台主机,并且端口号为80.
正确思路
本次实验要我们实现的是一个LRU-K算法的页面置换器。
-LRU-K算法是对LRU算法和LFU算法的折中优化,平衡了LFU和LRU的性能和开销的同时,也解决了缓存污染问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。具体来说,它维护了一个
+backward k-distance
,其计算方法:- 基于请求/响应模型的:一次请求对应一次响应
+- 无状态的:每次请求之间相互独立,不能交互数据
+历史版本:
++
+- 1.0:每一次请求响应都会建立新的连接
+- 1.1:复用连接
+报文格式
请求
客户端发送给服务器端的消息
+数据格式:
-
+- 如果已经被访问过k次:
-backward k-distance
=current_timestamp_
- 倒数第k次访问的时间戳- 如果还没被访问过k次:
+backward k-distance
= +inf- +
请求行
+
请求方式 请求url 请求协议/版本
GET /login.html HTTP/1.1+
+- 请求方式:
++
+- HTTP协议有7中请求方式,常用的有2种
++
+- GET:
++
-- 请求参数在请求行中【在url后】。
+- 请求的url长度有限制的
+- 不太安全
页面驱逐规则:
+- POST:
++
+- 请求参数在请求体中
+- 请求的url长度没有限制的
+- 相对安全
+- +
请求头:客户端浏览器告诉服务器一些信息
+
请求头名称: 请求头值+
+- +
常见的请求头:
-
-驱逐
-backward k-distance
最大的页。也即情况2总是优先会比情况1被驱逐;每次优先驱逐previous k次访问最早的页面。
+- -
User-Agent:浏览器告诉服务器,我访问你使用的浏览器版本信息 * 可以在服务器端获取该头的信息,解决浏览器的兼容性问题
当有多个页值为+inf,则采取FIFO规则进行驱逐。
+- +
Accept:可以支持的响应格式
+- +
Accept-language:可以支持的语言环境
+- +
Referer:http://localhost/login.html * 告诉服务器,我(当前请求)从哪里来?
++
+- 作用:
++
+- 防盗链:
+如果ref头非合法就不播放
- 统计工作:看从哪个网站来的人数多
+Connection:连接是否活着
故而,在具体实现中,为了便于管理,我将此拆分为两个队列:
-- -+
- +
请求空行
+
空行,就是用于分割POST请求的请求头,和请求体的。- +
请求体(正文):
++
+- 封装POST请求消息的请求参数的
+字符串格式:
++ + + +
//请求行
POST /login.html HTTP/1.1
//请求头
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Referer: http://localhost/login.html
Connection: keep-alive
Upgrade-Insecure-Requests: 1
//请求空行
//请求体
username=zhangsan响应
响应消息:服务器端发送给客户端的数据
+数据格式:
-
数据第一次被访问,加入到访问历史列表;
+响应行
++
- -
组成:协议/版本 响应状态码 状态码描述
HTTP/1.1 200 OK
如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;
+响应状态码:服务器告诉客户端浏览器本次请求和响应的一个状态, 状态码都是3位数字. 分类:
++
- -
1xx:服务器接收客户端消息,但没有接收完成,等待一段时间后,发送1xx多状态码,询问是否还要继续发
当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;
+- -
2xx:成功。代表:200
缓存数据队列中被再次访问后,重新排序;
+- -
3xx:重定向。代表:302(重定向),304(访问缓存)
++
需要自动重定向到另一个C去
++
发现资源未变化且本地有缓存
需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。
+4xx:由客户端造成的错误
+代表:
++
-- +
404(请求路径没有对应的资源,可能路径输错了)
405:请求方式没有对应的doXxx方法
+当我们在Servlet中未重写doXXX方法,就默认不能用此方法进行访问。因为doXXX方法的默认实现为:
+
String protocol = req.getProtocol();
String msg = lStrings.getString("http.method_get_not_supported");
if (protocol.endsWith("1.1")) {
resp.sendError(405, msg);
} else {
resp.sendError(400, msg);
}
每个页面结构持有一个时间戳队列即可:
-task1的内容就是实现对一堆frame_id
的LRU-K算法管理,挺简单的(也可能是测试用例少我错误没排查出来2333)
我并没有用默认给的模板的unorder_map
,也没有用默认给的模板思路(但原理以及最终效果是差不多的,就是没用它的方法),而是选择类似像上面这张图一样,分成两个队列实现,一个队列visit_record_
存储那些访问次数<k的数据,另一个队列cache_data_
存储那些访问次数>=k的顺序,每次优先淘汰visit_record_
中的数据,两个队列都采用LRU的方式管理。与此同时,我觉得LRU管理时间戳只用记录最新访问的就行,所以将历史访问时间戳队列改成了只有一个变量。
--参考:
-FIFO和LRU这里面的实例非常直观地说明了两种算法的差异,可以跟着手推感受一下
-pro1这个用的是我上面的那个想法,是错的。但是评论很值得参考:
--
pro1这个评论的“偷测试用例”xswl,虽然这次没用,但以后说不定能用上:
--
……简单个屁!!
-算法上,上面错误的算法确实很简单;而正确的算法也确实很简单。那么难的是什么呢?我觉得难的还是搞清楚它要我们实现的究竟是上面东西。
-结合指导书这段话:
---The LRU-K algorithm evicts a frame whose backward k-distance is maximum of all frames in the replacer. 每次驱逐
-backward k-distance
最大的那么
-backward k-distance
是什么?Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access.
-backward k-distance
=current_timestamp_
- 倒数第k次访问的时间戳A frame with fewer than k historical accesses is given +inf as its backward k-distance. 没有达到k次访问的,
-backward k-distance
为+inf。也就是说,每次优先从历史访问队列清除元素。【**When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**】当历史访问队列有多个元素,就驱逐英文描述那样的frame。
-
我们可以发现,它这个对于两个队列的LRU,并非我们原来算法那样,对于每个frame,针对其最新的访问时间戳,也即history_.back()
,进行LRU淘汰;而是,针对其倒数第k新的访问记录,也即history_.front() && history_.size()<=k
,进行LRU淘汰。
其中,由于历史访问队列的记录少于k个,因而其事实上从k-distance算法退化为了FIFO算法。【感受一下这一点的优美:FIFO实际上是k-distance的特例】
-我们上面的算法比较的是history_.back()
,所以可以省略时间戳队列为一个变量,然后将两个队列使用FILO的形式组织起来。正确算法就不能这么简单了,要按front排序的话,实现开销可能更大,所以下面就采用了map形式来实现logn的查找。
这里一个点我其实还是很疑惑的,完全想不通。
-就是,对缓存队列实现k-distance算法没毛病,这段话已经写得很清楚了。
---Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access.
-backward k-distance
=current_timestamp_
- 倒数第k次访问的时间戳
但是,为什么历史访问队列要用FIFO呢?是我英语不好吗,这段话不是实现纯正LRU的意思吗:
---【**When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**】当历史访问队列有多个元素,就驱逐英文描述那样的frame。
-
我翻译一下:
-当多个frame有+inf这个 backward k-distance
的时候,replacer需要驱逐拥有全部(overall)frame中最早的timestamp的frame。(也就是说,frame,它的最近访问记录是所有frame里面最早的)
这样确实看起来就是要用LRU。
-但其实,是我英语不好。咨询了场外热心人士hhj之后,我才修订出了如下版本:
-当多个frame有+inf这个 backward k-distance
的时候,replacer需要驱逐拥有全部(overall)frame中最早的timestamp的frame。(也就是说选择一个frame,这个frame的最不近的访问记录,是所有frame中最近最少访问的)【也即这个frame的history的front是所有frame中最早的,也即使用FIFO算法】
可见,正确解法确实是没问题的,就是理解上很困难。要是可以配个实例就好了QAQ
-所以说,所谓LRU(Least Recently Used)的直译还是最不近使用,也即最近最少使用。里面这个least不是用来修饰recent表示recent程度深的,相反它表示的是recent的程度浅。英语不好的惨痛教训啊。
-最后一下子交了这么多次才过。绷不住了。
---The
-BufferPoolManager
is responsible for fetching database pages from theDiskManager
and storing them in memory.从DiskManager中取出页,然后存入内存。也就是说,我们的Buffer Pool是磁盘到内存的映射,我们在Task1实现了内存部分的管理数据结构?
-The
-BufferPoolManager
can also write dirty pages out to disk when it is either explicitly instructed to do so or when it needs to evict a page to make space for a new page.也要负责dirty页的写回You will also not need to implement the code that actually reads and writes data to disk (this is called the
-DiskManager
in our implementation). We will provide that functionality.DiskManager
已给出All in-memory pages in the system are represented by
-Page
objects. EachPage
object contains a block of memory that theDiskManager
will use as a location to copy the contents of a physical page that it reads from disk.Page
是可复用的内存页容器The
-Page
object’s identifer (page_id
) keeps track of what physical page it contains; if aPage
object does not contain a physical page, then itspage_id
must be set toINVALID_PAGE_ID
.也就是说,
-page_id
表示的是实际的物理页号;frame_id
表示的是你的Page容器的序号,同时也是LRU的对象。你需要一个类似<fid, pid>
这样的map来记录这二者的映射。具体是通过:- -
/** Array of buffer pool pages. */
Page *pages_; // <fid, pid>
/** Page table for keeping track of buffer pool pages. */
std::unordered_map<page_id_t, frame_id_t> page_table_; // <pid, fid>Each
-Page
object also maintains a counter for the number of threads that have “pinned” that page. YourBufferPoolManager
is not allowed to free aPage
that is pinned. 有引用计数机制Each
-Page
object also keeps track of whether it is dirty or not. It is your job to record whether a page was modified before it is unpinned. YourBufferPoolManager
must write the contents of a dirtyPage
back to disk before that object can be reused.需要track dirty,并且这是你要干的;要写回,这也是你要干的Your
-BufferPoolManager
implementation will use theLRUKReplacer
class that you created in the previous steps of this assignment. TheLRUKReplacer
will keep track of whenPage
objects are accessed so that it can decide which one to evict when it must free a frame to make room for copying a new physical page from disk. When mappingpage_id
toframe_id
in theBufferPoolManager
, again be warned that STL containers are not thread-safe.
task2说得比较复杂,实现的函数较多,实际coding细节也比较繁琐,但debug倒是很轻松。
-主要内容就是实现BufferPoolManager
,在task1实现的LRU-K算法的基础上,写具体的内存换入换出的接口逻辑。
再次回顾我们整个project1的目的:实现一个从磁盘到内存的buffer。task1只是实现了一个内存页换入换出的LRU-K算法部分,task2则基于算法部分,实现了具体与上层交互的像样的逻辑。
-我认为这其中一个亮点就是,它非常完美地将LRU-K算法和具体的上层逻辑进行了解耦。LRU-K只需关注如何将这一堆freme_id
组织起来组织好,而无需关心具体内存页存放在哪,以及对应frame淘汰之后内存页又何去何从,因为这些逻辑都会由上层实现;而上层逻辑也无需关心具体的淘汰页算法【LRU-K/LRU/LFU,只需替换replacer_就可以替换换入换出策略】,而只需打好evictable标记,并且在调用evict方法之后做好后处理(如内存释放等等等)即可。
这其中有一个小细节也值得借鉴,即从page_id_
到frame_id_
的转化。frame_id_
有界,比较方便LRU-K算法实现,并且进行了LRU-K算法的容量控制,同时由于算法和上层逻辑的容量相同,故而也是pages_
的索引号;而page_id_
不能有界,因为实际上访问到的物理页不可能只共享pool_size_
个序列号。故而在这样解耦实现的基础上,二者缺一不可。
还有frame_id_
的复用,它是采用了类似我们日常生活中取号那样,要用号时从队列头取,不用号时塞回队列尾就行,这种方式我觉得还挺有意思。
其他部分虽然步骤繁杂,但理解难度不高,而且它提示得也很保姆了,所以不多bb。
-确实算简单了,我主要倒在没有认真看它的需求,这应该是语文问题(绷
-一个是FetchPage
这里:
如果所求物理页存在于buffer pool,直接返回+record access即可,不用再写回+读入。因为它的提示这边:
-这个是句号。也就是说后面那些写回啊read啊,是没找到时才做的,不是并列关系。
-这也很合理,毕竟你找到所需页就说明不用从磁盘读入,也即找到所需页=直接返回即可。
-另一个是UnpinPage
这里:
不应该写is_dirty_ = is_dirty
,因为它的提示这边:
可见参数is_dirty
为true是需要设置为dirty,为false的话没有别的意义,保持原来值就行。
还有一个就是,在Page
类中声明了friend:
故而BufferPoolManager
可以直接访问Page
的私有成员变量,而无需手动为Page
添加Getter/Setter方法。
这是要写我们在上面用的那个PageGuard?这让我想起了Lab0的ValueGuard
。
template <class T> |
5xx:服务器端错误。
+代表:500(服务器内部出现Exception)
+int i = 3/0; |
响应头:
+格式: [头名称 : 值]
+常见的响应头:
+Content-Type:服务器告诉客户端本次响应体 数据格式以及编码格式
+浏览器依照编码格式来对该页面进行解码。
+Content-disposition:服务器告诉客户端以什么格式打开响应体数据
+响应空行
+响应体:传输的数据
+字符串格式:
+//响应行 |
不过其实这两个是不一样的。本次要实现的Page Guard的语义更类似lock_guard
。
--我们需要手动调用
-UnpinPage
,但这中就跟new/delete、malloc/free一样都要靠人脑来记住,不大安全。You will implement
-BasicPageGuard
which store the pointers toBufferPoolManager
andPage
objects. A page guard ensures thatUnpinPage
is called on the correspondingPage
object as soon as it goes out of scope. 【也许这需要在析构函数中实现?】Note that it should still expose a method for a programmer to manually unpin the page.仍然需要提供UnPin方法。As
-BasicPageGuard
hides the underlyingPage
pointer, it can also provide read-only/write data APIs that provide compile-time checks to ensure that theis_dirty
flag is set correctly for each use case.这个思想很值得学习。In the future projects, multiple threads will be reading and writing from the same pages, thus reader-writer latches are required to ensure the correctness of the data. Note that in the
-Page
class, there are relevant latching methods for this purpose. Similar to unpinning of a page, a programmer can forget to unlatch a page after use. To mitigate the problem, you will implementReadPageGuard
andWritePageGuard
which automatically unlatch the pages as soon as they go out of scope.
怎么说,其实只用仔细看相关文档和它的要求就不难,但你懂的我的尿性就是不细看文档,所以这里我也用gdb调了蛮久才过的。正确思路没什么好说的,直接记录下我觉得比较有意义的错误吧。
-在这个用例中,退出“}”会调用两次析构函数。
-我在coding的过程中,遇到了一个很神奇的死锁现象。
-在这里page->WLatch();
这句会死锁,而且还是在第一次调用FetchWritePage()
时死锁的:
WritePageGuard(BufferPoolManager *bpm, Page *page) : guard_(bpm, page) { |
但是添加了一句page->WUnlatch();
:
WritePageGuard(BufferPoolManager *bpm, Page *page) : guard_(bpm, page) { |
它就不会死锁了。
-这很奇怪,到底是发生了什么?我用GDB调了半天,在RWLatch.WLock()
处打了断点,也没发现在这之前有调用过lock()。于是我就去看了下std::shared_mutex
的官方文档(当然,这中间想了很久也不知道怎么办):
我就怀疑是不是我哪里写错了,所以就干了这种undefined的事,然后就导致死锁了。于是我写了个测试程序:
-发现,当在调用WLock
(也即std::shared_mutex::lock()
)之前,如果多调了一次XUnlock
(也即std::shared_mutex::unlock()
或者std::shared_mutex::unlock_shared()
),就会卡住。
这说明确实发生了不匹配问题。于是我就在Page
中添加了两个成员变量用来记录上锁和解锁的次数,并且在gurad test中打印了出来,结果发现:
确实发生了不匹配问题,是在这里:
-之后用gdb调下就发现错误了,不赘述了。
-在出现死锁问题时,我是想着,会不会是测试程序中,对同一页获取了一次ReadGuardPage
对象之后,再对同一页获取Read/WriteGuardPage
导致的呢?于是我就开始思考如何防范这个流程,最后写下了这样的代码:
auto BufferPoolManager::FetchPageRead(page_id_t page_id) -> ReadPageGuard { |
ServletRequest(I) - HttpServletRequest(I) - RequestFacade(C)[tomcat创建]
+获取请求方式 POST
+String getMethod() |
获取虚拟目录 /webdemo
+String getContextPath() |
获取Servlet路径 /demo1
+String getServletPath() |
获取get方式请求参数 name=zhangsan
+&分割每个键值对
+String getQueryString() |
获取请求URI和URL
+// /webdemo/demo1 |
但很遗憾的是,我发现是无法区分当前进程持有write还是read锁的。也许有别的办法但我没想起来。
-总之,我认为这段代码还是很有参考价值的,姑且放着先。
--参考:
-CMU 15-445 2023 P1 优化攻略 [rank#3] 写得非常细致,思路很清晰
- -
--我的实现有一些并发小问题,详见lab2的并发部分~
+URL:统一资源定位符 : http://localhost/day14/demo1 中华人民共和国
+
URI:统一资源标识符 : /day14/demo1 共和国URI的代表范围更大
lru-k的算法优化是自己想的,并行IO的优化思路全部来自 CMU 15-445 Project 1 (Spring 2023) 优化记录,我只是把这位大佬的思路自己实现了一遍。感觉还是太菜了,面对这种实际场景毫无还手之力一点思路没有QAQ但正是如此,这个细粒度化锁的小task才值得学习。
-放上优化前后性能对比:
--In the leaderboard test, we will have multiple threads accessing the pages on the disk. There are two types of threads running in the benchmark:在具体的benchtest中,可以分为两类线程。
--
- Scan threads. Each scan thread will update all pages on the disk sequentially. There will be 8 scan threads.
-- Get threads. Each get thread will randomly select a page for access using the zipfian distribution. There will be 8 get threads.
+
获取协议及版本 HTTP/1.1
+String getProtocol() |
获取访问的客户机的IP地址
+String getRemoteAddr() |
Given that get workload is skewed(有偏向性的)(i.e., some pages are more frequently accessed than others), you can design your LRU-k replacer to take page access type into consideration, so as to reduce page miss.
- -我们可以回想起当初选择LRU-K而不选择LRU算法的原因:缓存污染。
-- --缓存污染:
-LRU因为只需要一次访问就能成为最新鲜的数据,当出现很多偶发数据时,这些偶发的数据也会被当作最新鲜的,从而成为缓存。但其实这些偶发数据以后并不会是被经常访问的。
-
而在这里也是同理。我们的benchtest中,scan线程是顺序地访问磁盘上所有页,而get线程是遵从zip分布地访问,显然get线程的access记录比scan线程的有价值的多,并且scan线程的数据是很容易污染get线程的。
-所以,我的解决方法是,如果某个页被第一次访问,且该访问方式为SCAN,则RecordAccess进入历史访问队列;如果某个页不是被第一次访问,且访问方式为SCAN,则不做任何处理。不用修改UnpinPage的处理方式。
---Instead of holding a global lock when accessing the disk manager【不要在访问
-disk_manager_
的时候使用bpm的全局锁latch_
】, you can issue multiple requests to the disk manager at the same time. This optimization will be very useful in modern storage devices, where concurrent access to the disk can make better use of the disk bandwidth.
详细的解决方法大佬这边已经说得很清楚了,接下来我就对其总体的做法进行一点总结,加上一些个人理解。
-我刚看到这个需求的时候是这么做的:
-if (pages_[fid].IsDirty()) { |
通过请求头的名称获取请求头的值
+String getHeader(String name) |
获取所有的请求头名称
+Enumeration<String> getHeaderNames() |
也即在原来代码的基础上做简单的改动,每次执行到涉及磁盘读写的地方,就暂时地开一下锁。但其实这样是不行的,当多个线程访问bpm,线程A在这里开锁执行Write,线程B正好得到锁,然后对pages_[fid]
执行比如说ResetMemory操作,这样就寄了。
所以,在磁盘读写的时候,我们仍然需要使用锁保护,只不过我们需要选择粒度更细的锁。这时我们就可以想到在page_guard
里常用的page自带的锁。在这里用page锁,既能够锁保护,又符合语义,看起来非常完美:
pages_[fid].WLatch(); |
返回的是一个迭代器
+public class Servletdemo2 extends GenericServlet { |
但由于我们在returnpage_guard
的时候会获取锁,因而在这样的情况下,会发生死锁:
auto reader_guard_1 = bpm->FetchPageRead(page_id_temp); |
这些请求头名称正是上面的键值对里的键。
+request将请求体中的数据封装成了流。如果数据是字符,那就是字符流;是视频这种的字节,那就是字节流。
+* 步骤:
+ 1. 获取流对象
+ * BufferedReader getReader():获取字符输入流,只能操作字符数据
+ * ServletInputStream getInputStream():获取字节输入流,可以操作所有类型数据
+ 2. 操作流获取数据
+
+
|
请求体中键值对会在一行里,用&分割
--在这里我们首先获取
-reader_guard_1
,持有了该 page 的读锁,并允许其他线程读;但在获取reader_guard_2
时,FetchPage
会在释放 bpm 写锁前,请求该 page 的写锁;但由于reader_guard_1
已经申请了该 page 的读锁,就会造成死锁,与预期结果不符。
因而,我们就可以选择在bpm内部,单独为pages_数组的每一页都维护一个锁,在每个对page页属性进行读写的地方进行锁定:
-std::shared_mutex latch_; |
然后对代码进行重排序,尽量分离bpm内部成员和page内部成员属性的修改:(以FetchPage
为例)
auto BufferPoolManager::FetchPage(page_id_t page_id, [[maybe_unused]] AccessType access_type) -> Page * { |
获取时中文乱码
+get方式:tomcat 8 已经将get方式乱码问题解决了
+post方式:会乱码
+ * 解决:在获取参数前,设置流的编码:
-其他地方也是一样。就不多赘述了。
-一个小地方
当外界需要对页进行读写时,需要使用page自带的锁;而当bpm内部需要对页进行读写时,则使用的是bpm内部自带的页锁。
-这句话说完,相信危险性已经显而易见了:我们使用了两把不同的锁维护了同一个变量!而且可能会有两个线程分别持有这两个锁,对这个变量并发更新!
-但其实,在当前这个场景,这么做是没问题的。
-外界实质上只能对page的data字段进行读写。因而,有上述危险的,实质上就只有bpm中会对data字段进行改变的地方,也即bpm::NewPage()
、bpm::FetchPage()
、bpm::DeletePage()
这三个地方。
-而在前两个地方,我们会使用到的page都是闲置/已经被释放的页,因而外界不可能,也即不可能有别的线程,会持有page的锁并且对其修改;同样的,在第三个地方,我们会使用的page也是pincount==0的页,仅有当前线程在对其进行读写。
-因而,综上所述,这样做是并发安全的。
-]]>
参考
-CMU 15-445 Project 2 (Spring 2023) | 关于 B+Tree 的十个问题
- - ---In this programming project you will implement a B+Tree index in your database system.
-Your implementation will support thread-safe search, insertion, deletion (including splitting and merging nodes包括分裂和合并结点), and an iterator to support in-order leaf scans.
++ + +
request.setCharacterEncoding("utf-8");
它这里的B+树(以及wiki里的)跟王道考研讲得不大一样。王道考研的B+每个结点一个关键字对应一个child,但是这里的是B树的形式。
---You must implement three Page classes to store the data of your B+Tree:
+获取请求参数通用的方法(通用指对get和post通用)
这里的请求参数应该是指上面Post的请求体、Get的请求行里的参数,请求头里的参数是获取不到的。
-
-- -
B+Tree Page
-BPlusTreePage
下面那两个的基类
-B+Tree Internal Page
-An Internal Page stores m ordered keys and m+1 child pointers (as page_ids) to other B+Tree Pages.These keys and pointers are internally represented as an array of key/page_id pairs.
-Because the number of pointers does not equal the number of keys, the first key is set to be invalid, and lookups should always start with the second key.
-At any time, each internal page should be at least half full.【min_size<= <=max_size】
-During deletion, two half-full pages can be merged, or keys and pointers can be redistributed to avoid merging. During insertion, one full page can be split into two, or keys and pointers can be redistributed to avoid splitting.
+- -
根据参数名称获取参数值
++ +
String getParameter(String name)如 username=zs&password=123,getParameter(“username”)会得到zs。
B+Tree Leaf Page
-The Leaf Page stores m ordered keys and their m corresponding values. In your implementation, the value should always be the 64-bit record_id for where the actual tuples are stored; see the
-RID
class, insrc/include/common/rid.h
.*Note:* Even though Leaf Pages and Internal Pages contain the same type of key, they may have different value types. Thus, the
+max_size
can be different.- +
根据参数名称获取参数值的数组
++ +
String[] getParameterValues(String name)如 hobby=xx&hobby=game,会得到{xx,game}
- +
获取所有请求的参数名称
+
Enumeration<String> getParameterNames()取所有参数的map集合
+
Map<String,String[]> getParameterMap()
大概就是有一个基类结点,它有两个子类,一个表示b+树的leaf node,另一个表示b+树的internal node,每个结点都占据一个内存页。
-也就是说,一个内存页中存储着一个结点类对象。每次我们都是读取一页到内存中,然后将它类型转换为TreeNodePage*,就可以访问其里面的存储数据的数组array_
了。体会一下这个思想。
值得一提的是,LeafPage
的成员变量中有个这样的成员:
private: |
在服务器内部资源跳转。
+AServlet做了一部分事情,把剩余的事情交给BServlet去做
+步骤:
+通过request对象获取请求转发器对象
+RequestDispatcher getRequestDispatcher(String path) |
使用RequestDispatcher对象来进行转发
+requestDispatcher.forward(ServletRequest request, ServletResponse response) |
|
它就是柔性数组成员。
+特点:
+接力工作的两个Servlet可以通过request对象进行数据通信。
+* 方法:
+
+存储键值对
+void setAttribute(String name,Object obj) |
获取值
+Object getAttitude(String name) |
移除键值对
+void removeAttribute(String name) |
ServletContext getServletContext() |
++要求:
+1.编写login.html登录页面
+
username & password 两个输入框
2.使用Druid数据库连接池技术,操作mysql,day14数据库中user表
3.使用JdbcTemplate技术封装JDBC
4.登录成功跳转到SuccessServlet展示:登录成功!用户名,欢迎您
5.登录失败跳转到FailServlet展示:登录失败,用户名或密码错误
![屏幕截图 2023-01-02 235207](./JavaWeb/屏幕截图 2023-01-02 235207.png)
-+在C++中,Flexible Array Member(柔性数组成员)是一种用于定义具有可变大小的结构的技术。它通常用于在结构的末尾声明一个数组,该数组的大小是动态确定的,这允许你在使用该结构时更灵活地处理变长数据。
-在你提供的代码片段中,
-MappingType array_[0];
是一个柔性数组成员的例子。这里array_
后面有[0]
,这并不表示它们的大小是固定的0。相反,它们的大小是在运行时动态确定的,而[0]
的写法是一种历史上的技巧,用于告诉编译器这是柔性数组成员。例如,如果有一个结构定义如下:
-+
struct MyStruct {
// 其他成员...
MappingType array_[0];
};错误历程
++
+- +
lib目录位置错误
+NoClassDefFoundError解决方案一开始lib目录没放进web-inf,通过此文章得知错误为包未引入,再由下面这篇文章得知lib目录放置错误
+JDBC Template报错:java.lang.ClassNotFoundException: org.springframework.jdbc.core.RowMapper
+- +
druid.properties文件位置错误
+报错
++
java.lang.NullPointerException at java.util.Properties$LineReader.readLine(Properties.java:434)
,报错位置在
+pro.load(JDBCUtils.class.getClassLoader().getResourceAsStream("druid.properties"));
由文章
+关于java.lang.NullPointerException at java.util.Properties$LineReader.readLine(Properties.java:434)问题
+回忆到,由于是使用类加载器获取文件流,故而要求druid.properties文件应该放在resource文件下。对于以前的项目,resource文件都默认是src文件夹。
+但是这次放在src目录下还是不行。定睛一看它web项目文件结构中有一个硕大的resources……放在下面果然就好了。
+
driverClassName=com.mysql.jdbc.Driver |
你可以根据需要为 array_
分配任意数量的内存,例如:
int arraySize = 10; // 你想要的数组大小 |
|
在这个例子中,array_
可以用于存储可变大小的数据,而结构体 MyStruct
的大小将动态地调整为 sizeof(MyStruct) + arraySize * sizeof(MappingType)
。这样的设计通常在需要处理变长数据块的场景中比较有用。请注意,在C++17之后,你也可以使用 std::byte
类型来定义柔性数组成员。
拥有柔性数组成员的实例需要动态分配内存(或者像接下来的把一块内存空间interpret一下),柔性数组成员会占用其他成员没有占用的剩下的空间,也即:
-+--------------------------+ |
|
|
|
--The index should support only unique keys; if you try to reinsert an existing key into the index, it should not perform the insertion, and should return false. key必须unique
-B+Tree pages should be split (or keys should be redistributed) if an insertion would violate the B+Tree’s invariants. 插入时需要分裂
-If an insertion changes the page ID of the root, you must update the
-root_page_id
in the B+Tree index’s header page. You can do this by accessing theheader_page_id_
page, which is given to you in the constructor. Then, by usingreinterpret_cast
, you can interpret this page as aBPlusTreeHeaderPage
(fromsrc/include/storage/page/b_plus_tree_header_page.h
) and update the root page ID from there. You also must implementGetRootPageId
, which currently returns 0 by default.对root_page_id
的一切访问,都需要通过header_page_id_
。如果插入后改变了root的page ID,需要更新root_page_id
。We recommend that you use the page guard classes from Project 1 to help prevent synchronization problems. For this checkpoint, we recommend that you use
-FetchPageBasic
(defined insrc/include/storage/page/
) when you access a page. 在当前task中,我们推荐你使用pro1实现的page guard,比如说这里如果要访问一页,就需要用FetchPageBasic
。You may optionally use the
-Context
class (defined insrc/include/storage/index/b_plus_tree.h
) to track the pages that you’ve read or written (via theread_set_
andwrite_set_
fields) or to store other metadata that you need to pass into other functions recursively.你可以随意使用和修改Context
class,它大概就是一个存储共享信息的对象。If you are using the
-Context
class, here are some tips:如果你要用,要注意以下几点:-
-- -
You might only need to use
-write_set_
when inserting or deleting. 当你在为B+树插入/删除结点时,需要用到write_set_
。【为什么?这个set存储的是修改路径上的结点吗?然后如果要分裂/合并结点,只需什么while(pop且需要分裂/合并){分裂/合并}??所以说这里的deque是栈结构?】也就是说,其实我们就可以不用递归了,而是将上下文存储在context->write_set_这个栈里面就行了?大概是这个意思吧
-It is possible that you don’t need to use
-read_set_
, depending on your implementation.read可以用递归(比较简单)也可以不用,所以说具体看实现。
-- -
You might want to store the root page id in the context and acquire write guard of header page when modifying the B+Tree.你需要将root page id存储在context,并且在修改b+树(插入、删除)时获取header page的WritePageGurad。
-- -
To find a parent to the current node, look at the back of
-write_set_
. It should contain all nodes along the access path.如果想要寻找当前node的父亲,可以看看write_set_.back
,它包含了访问路径上所有结点的引用【所以确实是当成栈来用了】- -
You should use
-BUSTUB_ASSERT
to help you find inconsistent data in your implementation. 需要使用BUSTUB_ASSERT
。For example, if you want to split a node (except root), you might want to ensure there is still at least one node in the
-write_set_
. If you need to split root, you might want to check ifheader_page_
isstd::nullopt
.如果你想要分割一个根节点以外的node,那你必须保证
-write_set_
中至少有一个结点;如果你想要分割根节点,那你必须保证header_page_
非空。- -
To unlock the header page, simply set
-header_page_
tostd::nullopt
. To unlock other pages, pop from thewrite_set_
and drop.如果你想要不锁住header page,那就置其为空指针;如果想释放别的页,那就将它从write_set_
pop出来就行。【这是因为我们要用到的page类型都是page guard,可以析构时unpin吗?】
由于各种原因,lab2的战线还是拉得太长了。四月份完成了代码初版,中间修了几个bug勉强通过了insertion test,然后一直到十一月底的现在才再次捡起来。不得不说,回看当初的代码,还是能够很清晰地感受到自己这半年多来的成长的,令人感慨。
-我先是花了一天的时间重构了下以前写的所有代码,然后再花了两天时间修bug终于通过了insertion test和sequence scale test,并且将b+树的代码修到了我满意的地步(指不像以前那样一坨重复代码和中文注释。。。)。
-这里简要介绍下B+树的插入实现及我觉得实现中需要注意的几个要点吧。
-B+树的插入流程大概是这样的:
+++关于BeanUtils
+BeanUtils工具类,简化数据封装, 用于封装JavaBean的
-
查找到key要插入的叶子结点(途中需要维护write_set,也即查找路径)
+- -
JavaBean:标准的Java类
+1. 要求: + 1. 类必须被public修饰 + 2. 必须提供空参的构造器 + 3. 成员变量必须使用private修饰 + 4. 提供公共setter和getter方法 + 2. 功能:封装数据 +
- -
判断结点是否满
--
-未满,直接插入即可。(我采取插入排序的方法)
+- -
概念:
+ 成员变量:
+
属性:setter和getter方法截取后的产物例如:getUsername() --> Username--> username +
已满,需要对结点进行分裂。
-推举出中间结点tmp_key,它和新结点page_id接下来将插入到父节点中。
+方法:
++1. setProperty() +1. getProperty() +1. populate(Object obj , Map map): +
将map集合的键值对信息,封装到对应的JavaBean对象中
持续进行分裂:
-需要注意具体的分裂方法,我认为其中internal page size == 3的情况尤为棘手。在具体实现中,我是这样分裂的:
+
原封不动地照搬了:第二部分-数据库连接池-Druid-定义工具类 部分的代码。
+public class UserDao { |
设置响应消息。
+设置状态码
+setStatus(int sc); |
setHeader(String name, String value) |
以流的方式传输数据。
+使用步骤:
推举出将要被插入到父节点的tmp_key
-该推举出的key将不会出现在分裂后的新旧结点中,而是会被加入到父节点中。默认为(m + 1) / 2
【m为max size】。
但是要尤其注意size为3的case,此时tmp_key为array_[2]
,很有可能右边结点为空。所以我们需要做点特殊处理:
获取输出流
insert_key > array_[(m + 1) / 2]
时,我们推举(m + 1) / 2
这个结点。insert_key < array_[m / 2]
,我们转而推举m / 2
(此时为array_[1]
)。insert_key < array_[(m + 1) / 2]
且insert_key > array_[m / 2]
时,我们应该对此做出特殊处理,推举insert_key。在此为了代码实现方便,我们还需要调换insert_key和tmp_key的地位。字节输出流
+ServletOutputStream getOutputStream() |
字符输出流
+PrintWriter getWriter() |
special = false; |
分裂旧结点
-被推举出的tmp_key的value及其右部元素会变成新结点,左部依然留在旧结点,tmp_key会到父节点中去。也即如下图所示:
-![未命名文件 (1)](./cmu15445/未命名文件 (1).png)
-依然是注意上面那个case3特殊情况,需要交换insert key和middle key:
-if (!special) |
持续进行推举和分裂,直到父节点不用分裂
-此时直接将insert key和insert value插入排序到父节点即可。
使用输出流,将数据输出到客户端浏览器
然后是Iterator的话,我感觉这也是设计得很不错,让我们亲手写了下c++的重载运算符,也是让我学到了很多c++知识。。。
-感觉问题其实不多,主要还是debug有点痛苦花了很长时间()
-切换内核前后报错。
-Check for working C compiler: /usr/bin/cc - broken
-感觉可能是内核切来切去,导致cmake cache发生了点小问题?总之我最后在5.11内核把build文件删了,重新执行cmake -DCMAKE_CXX_COMPILER=$(which g++) -DCMAKE_C_COMPILER=$(which gcc) ..
就ok了。
我发现在这里创建的root最后好像会被释放掉?
-比如我看到新root的page为6,连接也做得好好的,最后出了函数就寄了:
-还有一个是发现新的leaf page好像不大对,其类型甚至是internal呃呃,我调下看看
-尼玛,绷不住了是这里:
-原来写的
-改了之后test2马上ok,乐
-还弄了个commit修:
-auto INDEXITERATOR_TYPE::operator*() -> const MappingType & |
资源跳转的一种方式。
+
|
这个函数卡了我还挺久。。。里面逻辑很简单,不过难就难在怎么构造出一个const MappingType &
。
如果这样:
-INDEX_TEMPLATE_ARGUMENTS |
|
会说你临时对象不能作为引用。如果这样:
-INDEX_TEMPLATE_ARGUMENTS |
输出: |
又会找不到机会delete导致内存泄漏。冥思苦想了半天不知道该怎么办,最后从网上看了别人怎么写的:
-INDEX_TEMPLATE_ARGUMENTS |
重定向的这几行代码其实是可以简化的:
+/* 重定向 */ |
我服了。
-不过可能有更好的解决方法?可惜我c++水平不大够,所以暂时想不出来了。
-由于有了insert的沉淀,remove的实现便相较不大困难了,写完代码到通过内置的delete测试只花了一天的时间。
-找到需要操作的叶结点路径
-判断叶子结点属于以下四种策略中的哪一种,执行对应策略(优先级从高到低):
-直接删除
-当删除后叶结点元素数仍在合法范围,并且路径上父节点没有target key,直接删除然后返回即可。
-更新父节点路径
-当删除后叶结点元素数仍在合法范围,并且路径上父节点有target key,直接删除然后向上回溯更新父节点即可。
-窃取兄弟元素
-If do a steal, we should update related key in the parent, and update up till reaching the root. |
可以简化为:
+resp.sendRedirect("/practice_war/demo2"); |
当删除后叶结点元素数过少,并且左右兄弟元素充足,则从左右兄弟窃取一个。优先窃取元素最多者。
+++关于req对象不一样,但hashcode值相同的解释:
+hashcode很大程度与对象内存空间相关,与对象的具体内容没什么关系。两个对象拥有相同的hashcode有可能只是因为存储的内存空间位置大小都相同导致的。所以是因为两次的req对象都占用了同一个内存空间【JVM调度问题】,所以才让hashcode值相同。这两个对象实质上是不一样的。
+
重定向的特点(与请求转发完全相反):
窃取左兄弟
-窃取左兄弟的最大元素
-需递归更新自身父节点路径上的对应值。
-窃取右兄弟
-窃取右兄弟的最小元素
-需要递归更新自身和右兄弟父节点路径上的对应值。
-之后返回即可。
-合并
-/* |
当删除后叶结点元素数过少,并且左右兄弟元素也都是最小值,那么需要与左右兄弟之一进行合并。优先合并左兄弟。合并都为大->小,也即target->左兄弟 或者 右兄弟->target。
-需要递归删除父节点路径上的merge from元素。
-路径写法:
+相对路径:通过相对路径不可以确定唯一资源
+可以看到,1/2/3三种情况都可以实现简单地直接返回。4稍显复杂,由于递归删除,所以需要对每一个父节点都再次进行上面几种策略的判断,直到遇到情况123返回为止。
+绝对路径:通过绝对路径可以确定唯一资源
+如:http://localhost/day15/responseDemo2 /day15/responseDemo2
+<form action="/webdemo4_war/check" method="post"> |
以/开头的路径
一个比较sb的小bug……
-这位可更是重量级,足足花了我三天的时间……不过感觉第一次处理这么一个复杂的并发情景,花的时间还是值得的。
-最后的结果虽然很一般(指排行榜倒数水平。。。),但至少还是过了。就先这样吧。
-我实现了crabbing lock+optimal lock。对于Insert和Remove,都是先在一开始获取header page和路径上父节点的读锁,然后在之后有可能向上更新时(比如说Insert的需要分裂、Remove的Update和Merge两种情况),丢弃所有读锁,然后获取header page和路径上父节点的写锁。
-不过感觉我这个思路还是略有粗糙,因为相当一部分时间都得占用header page的写锁。但是我思考了一下细粒度方案,发现还是有点难实现。比如说,对于insert,细粒度化的方式也许就是一直持有header page的读锁,一直到需要分裂根节点时,才释放读锁获取写锁。但这样一来就会暴露一个危险的空窗期(而且感觉这个空窗期还不小),当你真的拿到写锁,这树的结构可能已经变得不知道什么样了。在这种情况下,你就需要再做一次回溯工作,也即获取从新root结点到旧root结点的路径,递归插入insert key和insert value,最后安全分裂根节点(因为此时已经安全持有了header page写锁)。感觉思路也是比较易懂,但是实现上还是太麻烦了,所以先暂且搁置吧。
-这种感觉大多还是在面向测试用例见招拆招……所以其实感觉没什么好说的。
-这个并发问题是这样的,我原来是先evict,然后再写回被替换的页面,写回过程中磁盘没加bpm锁。这就会出现这样一个情况:
-一个page被进程A evict,进程A还没执行写回的时候这个page又被进程B捡回来了,因为还没写入所以磁盘空空如也。这时候pages_latch_这个细粒度锁不能防范这种情况,是因为此时这个page对应的container不是同一个,所以fid不同,细粒度锁不同导致寄。
-解决方法是要么写的时候持有bpm锁,但是这太太慢了。另一个就是干脆直接在unpin的时候不带bpm锁顺便写回了。也即把写回从evict后移到unpin中立即写回:
-if (pages_[fid].GetPinCount() == 0 && pages_[fid].IsDirty()) { |
- - --
看起来感觉大多性能损耗还是在bpm上,特别是LRU-K。也许是我的全局锁太暴力了。
-TODO,注意一下为什么每个executor的child executor的&&和&的差别
---In this project, you will implement the components that allow BusTub to execute queries. You will create the operator executors that execute SQL queries and implement optimizer rules to transform query plans.
-实现SQL查询的执行,并且实现语句优化。
-
--In this project, you will add new operator executors and query optimizations to BusTub.
-BusTub uses the iterator (i.e., Volcano) query processing model, in which every executor implements a
-Next
function to get the next tuple result.When the DBMS invokes an executor’s
-Next
function, the executor returns either:(1) a single tuple
- In BusTub’s implementation of the iterator model, 除了元组外还会返回record identifier (
-RID
)(2) an indicator that there are no more tuples.
-With this approach, each executor implements a loop that continues calling
-Next
on its children to retrieve tuples and process them one by one.
介绍完了bustub的框架之后,它对通过语法树进行查询优化进行了详细的样例介绍。
-首先温习一下什么是语法树(abstract syntax tree, AST ):
-SQL语句
-Select `title` |
其语法树表示+优化结果如下图所示:
-算法如下,其关键思路就是选择投影尽早做,能移多下去就移多下去
-而这里15445介绍的也是这样的语法树优化算法。
-首先记录一下它这几个专有名词对应的操作:
----
-- Projection:投影
-- Filter:选择
-- MockScan:对一个表进行的扫描操作
-- Aggregation:聚合函数
-- NestedLoopJoin:嵌套循环连接
-
再结合它给的几个语法树的例子:
-SELECT * FROM __mock_table_1; |
SELECT colA, MAX(colB) FROM |
SELECT * FROM __mock_table_1 WHERE colA > 1; |
values (1, 2, 'a'), (3, 4, 'b'); |
规则:判断定义的路径是给谁用的?判断请求将来从哪儿发出
+给客户端浏览器使用:需要加虚拟目录(项目的访问路径)
+比如说在页面中弄了个a标签,将来是要给客户端点的,那么这个a标签的href就要用绝对路径。
+再比如说重定向:
+//要填的是完整资源路径。 |
可以看到,它大概是用缩进来表示了AST的父子关系。
-我们课上学习的语法树中每个table标志对应着一个MockScan;笛卡尔积+选择操作可以表示为一个NestedLoopJoin。
-对于这些输出的意义,指导书也给了详细的解释:
-ColumnValueExpression
-也即类似exprs=[#0.0, #0.1]
,#0
意为第一个子节点(不是第一个表的意思。。)
--火山模型和优化(向量化执行、编译执行) 这篇文章写得很详细,下文也摘抄自该博客。
- -
火山模型又称 Volcano Model 或者 Pipeline Model(或者迭代器模型)。该计算模型将关系代数中每一种操作抽象为一个 Operator,将整个 SQL 构建成一个 Operator 树,从根节点到叶子结点自上而下地递归调用 next() 函数。
-一般Operator的next() 接口实现分为三步:
+这个路径将来是给客户端将来要使用的路径,是客户端路径,所以要加虚拟目录。
因此,查询执行时会由查询树自顶向下的调用next() 接口,数据则自底向上的被拉取处理。火山模型的这种处理方式也称为拉取执行模型(Pull Based)。
-大多数关系型数据库都是使用迭代模型的,如 SQLite、MongoDB、Impala、DB2、SQLServer、Greenplum、PostgreSQL、Oracle、MySQL 等。
-火山模型的优点是,处理逻辑清晰,简单,每个Operator 只要关心自己的处理逻辑即可,耦合性低。但是缺点也非常明显:
+给服务器使用:不需要加虚拟目录
+比如说之前的请求转发
每处理一行需要调用多次next() 函数,而next()为虚函数,开销大。
-编译器无法对虚函数进行inline优化,同时也带来分支预测的开销,且很容易预测失败,导致CPU流水线执行混乱。
+数据以行为单位进行处理,不利于CPU cache 发挥作用。
+火山模型显而易见是以从上到下一个流水线形式执行的,它的最理想情况是每个流水线节点所需的这个tuple都存储在寄存器中。然而,有一些操作,如聚合函数等等,需要对整个表进行操作才能获取到当前所需tuple,而整个表显然最多只能读入到内存中,这样的操作就被称为pipeline breaker。
-下面的实现中的aggregation、sort、hash join的build阶段都是pipeline breaker,这些复杂的操作阶段都需要在init()函数中进行。
-TODO,从宏观整个架构简介
---The entirety of the catalog implementation is in
-src/include/catalog/catalog.h
. You should pay particular attention to the member functionsCatalog::GetTable()
andCatalog::GetIndex()
. You will use these functions in the implementation of your executors to query the catalog for tables and indexes.
它意思大概是说在实现executor时可能需要用到catelog里这两个函数。
-GetTable
返回一个TableInfo
:
struct TableInfo { |
|
|
|
--For the table modification executors (
-InsertExecutor
,UpdateExecutor
, andDeleteExecutor
) you must modify all indexes for the table targeted by the operation. You may find theCatalog::GetTableIndexes()
function useful for querying all of the indexes defined for a particular table. Once you have theIndexInfo
instance for each of the table’s indexes, you can invoke index modification operations on the underlying index structure.In this project, we use your implementation of B+ Tree Index from Project 2 as the underlying data structure for all index operations. Therefore, successful completion of this project relies on a working implementation of the B+ Tree Index.
-
话说index是那个索引吗,就是每张表有几个建立在某个属性的索引,也即一张表可以有n棵b+树
-GetIndex
返回一个IndexInfo
:
struct IndexInfo { |
|
--The BusTub optimizer is a rule-based optimizer. Most optimizer rules construct optimized plans in a bottom-up way(自底向上). Because the query plan has this tree structure, before applying the optimizer rules to the current plan node, you want to first recursively apply the rules to its children.
-At each plan node, you should determine if the source plan structure matches the one you are trying to optimize, and then check the attributes in that plan to see if it can be optimized into the target optimized plan structure.
-In the public BusTub repository, we already provide the implementation of several optimizer rules. Please take a look at them as reference.
-
--In the background section above, we saw that the BusTub can already retrieve data from mock tables in
-SELECT
queries.This is implemented without real tables by using a
-MockScan
executor to always generate the same tuples using a predefined algorithm.This is why you cannot update these tables.
-
也就是说意思是目前的mockscan executor不是真的查表,而是返回固定的元组。
-看了一遍代码,感觉大概明白了。我们可以来看一下迭代器的Next函数:
-auto MockScanExecutor::Next(Tuple *tuple, RID *rid) -> bool { |
代表整个web应用,可以和servlet容器(服务器)通信
+ServletContext getServletContext() |
其核心就是调用func_来获取表的元组。
-也就是说是这样的,每个MockScanExecutor用来执行一个plan,那么也就对应着某一个table。通过执行某一个table特定的迭代function,就可以返回元组。
-这个迭代function比如说对于表tas_2023是这样的:
-if (table == "__mock_table_tas_2023") { |
this.getContext(); |
也即MockScanExecutor负责对表指针的管理,function负责实际对表的物理访问。这样就成功解耦了。
---In this task, you will implement executors that read from and write to tables in the storage system.
--
-- -
src/execution/seq_scan_executor.cpp
- -
src/execution/insert_executor.cpp
- -
src/execution/update_executor.cpp
- -
src/execution/delete_executor.cpp
- -
src/execution/index_scan_executor.cpp
而我们本次实验就是需要实现这么一大堆的executor。看来又是个体力活了。
-可以看到,前缀++重载的运算符方法和后缀++是不一样的。
---这里我理解得还是肤浅了…… 根据 这篇文章,
-++i
的内部类定义为T& T:: operator++();
,而i++
的内部类定义为T T:: operator++(int);
[1],前置操作返回引用,后置操作返回值。后置操作的int
参数是一个虚拟参数,用于区分运算符++
的前置和后置。理论上,i++
会产生临时对象,实践中,编译器会对内置类型进行优化;而对于自定义类型(如这里的 Iterator),++i
的性能通常优于i++
。
值得一提的是它跟MockScan的关系。MockScan是一种模拟操作,所以各种表都是硬编码在它的mock_scan.h里的;而SeqScan就是真正的遍历操作了,它需要获取tuple就需要通过各种复杂的物理操作和封装一步步读取了。
-通过实现SeqScan,我们可以初步窥探整个bustub物理层面交互的架构。
-跟之前project中的索引entry一样,实际的数据tuple也保存在page中,其对应类为TablePage
。并且是堆文件组织结构:
---
TablePage
的结构值得一提。在它的成员定义中,我们可以看到其中有两个柔性数组成员(Flexible array member):
-+
char page_start_[0];
TupleInfo tuple_info_[0];功能
获取MIME类型
MIME是在互联网通信过程中定义的一种文件数据类型
++* 格式: 大类型/小类型 text/html image/jpeg +
-
/*
@param: 文件的后缀扩展名,如.txt
*/
String getMimeType(String file);之前的Project2,我们只接触过一个的case,这里的两个感觉其实也同理可得,相当于
-page_start_
和tuple_info_
都指向最末尾空闲空间的开始。-
TablePage
的实际存储结构如下:+
-> increase increase <-
| ------------------------------- | ********************************* |
↑ ↑
page_start & tuple_info +TUPLE_INFO_SIZE*sizeof(TupleInfo)+
mime映射存在了服务器的xml文件中。
+使用案例:
+-
System.out.println(this.getServletContext().getMimeType("a.txt"));也即tuple info存储在前半部分,tuple data存储在后半部分,并且二者增长方式相反。
-
而多页TablePage
就构成了一个TableHeap
,也即其物理存储空间。每次创建表时,我们就会分配对应的heap空间和相关meta data。TableHeap
对外提供了增删改查元组的方法,也提供了一个迭代器实现TableIterator
,用于遍历里面的元素。
而由于元组tuple存储在磁盘中,所以我们需要在读取它的值的时候先进行反序列化DeserializeFrom
,这个过程需要用到表的类型信息和offset信息之类的,所以Tuple::GetValue
需要传入schema
参数。
它基本原理也就是顺序遍历整张表,没什么好说的。
-在本次的sequence scan实现中,我们就需要首先获取表对应的iterator:
-// 巨长一串 |
然后通过这个iterator不断迭代获取元素即可。
-有一点要注意的,应该是对删除元组的处理,毕竟sequence scan算是是实现其他二级操作的基石了,所以我们必须在这里处理删除元组。具体逻辑如下:
-do { |
ServletContext是一个域对象,可以用来共享数据。
+ServletContext代表着服务器,因而它的生命周期跟随服务器关闭而灭亡。ServletContext可以共享所有请求的数据。也就是说,任何一次请求,任何用户,看到的ServletContext域都是同一个。
+这样大的效果也使得我们需要更加谨慎地使用它。一旦数据存入ServletContext域,就只会在服务器关闭后才会消亡,很耗内存。
+String getRealPath(); |
经测试发现,这东西只是起了一个字符串拼接的作用,是不会帮忙检查文件是否存在的。
+学到这我顺便看了看文件放在不同的地方最后应该如何访问:
+这是最终部署项目文件夹的结构:
+可以看到只有bcd被保留了。它们的目录要这样获取:
+
|
对于SQL的嵌套子查询,bustub采用的是递归实现。具体来说,以insertion为例:
-外界调用情况如下所示。
-// Execute a query plan. |
用户点击下载->请求发送给某个servlet,servlet修改response->tomcat响应用户,传递的图片资源按照response的方法打开
+说实话看了感觉有点难以下手,主要还是完全不知道html和servlet怎么交互造成的,看了老师讲解才有点恍然大悟。
+我们可以把a标签以重定向的角度去看。它会新建一个request,然后发送到它的href中的那个url。在此处我们将url设置为/practice_war/download?filename=1.jpg
,也即要以GET的方式发送给download,请求体为filename=1.jpg
。然后servlet执行结束后,就会将信息存储在resp中返回给tomcat,由tomcat发送给用户。
<body> |
CreateExecutor
是一个递归函数,递归创建每个子查询的实例,把对应的executor返回给父查询
auto ExecutorFactory::CreateExecutor(...) |
思路:
+获取要下载的资源,并且将其输入到resp的stream中。
+有一点需要非常注意:
+resp.setContentType(this.getServletContext().getMimeType(path)); |
然后我们再在父查询的Init中调用子查询的Init和Next等方法
-void InsertExecutor::Init() { |
如此,就能递归实现嵌套子查询。
---The
-InsertExecutor
inserts tuples into a table and updates any affected indexes.The planner will ensure that the values have the same schema as the table. The executor will produce a single tuple of integer type as the output, indicating how many rows have been inserted into the table.
-
这里将Insert语句插入的值视为一个匿名子表,对其初始化后使用它的迭代器进行元素访问即可。
-bustub将一切表达式抽象为了这么几个类:
-AbstractExpression // 基类 |
而从UpdatePlanNode中,我们可以获取到update字句的所有表达式:
-/** The new expression at each column */ |
比如此处:
-bustub> explain (o,s) update test_1 set colB = 15445; |
然后我们分别计算每个expression的值,就可以获取更新之后的元组:
-// insert again |
删除元组的实现似乎只是简单地标记is_delete_
为true就好了。但是我在实际的代码实现(InsertTuple
)中似乎并没有看到重组删除空间or覆盖删除空间,每次插入页满只是简单地再申请新的一页,不会再回头。也许是为了简化起见暂不实现这个吧。
不过改进方法也很简单,对每个表进行固定分配页(或者说提供一个数据量达到百分之几的时候扩容的机制),然后页面间组织成环形链表,这样就能充分覆盖删除空间,同时也兼顾一定性能了。
-update的实现也不会很难,只需先删除原来的元组,再加个新元组即可。
-delete的实现完全照搬update就行,没什么好说的。
---The
-IndexScanExecutor
iterates over an index to retrieveRIDs
for tuples. The operator then uses these RIDs to retrieve their tuples in the corresponding table. It then emits these tuples one at a time.You can test your index scan executor by
-SELECT FROM <table> ORDER BY <index column>
. We will explain whyORDER BY
can be transformed intoIndexScan
in Task #3. 哦吼,也就是说order-by会被翻译为index scan?那order-by的关键字如果不存在索引会怎么样,现建吗BusTub only supports indexes with a single, unique integer column. Our test cases will not contain duplicate keys. The type of the index object in the plan will always be
-BPlusTreeIndexForTwoIntegerColumn
in this project. You can safely cast it and store it in the executor object:- -
using BPlusTreeIndexForTwoIntegerColumn = BPlusTreeIndex<IntegerKeyType, IntegerValueType, IntegerComparatorType>;
tree_ = dynamic_cast<BPlusTreeIndexForTwoIntegerColumn *>(index_info_->index_.get())但我看测试里怎么好像有两个键的index?
-You can then construct an index iterator from the index object, scan through all the keys and tuple IDs, lookup the tuple from the table heap, and emit all tuples in order.
-是的,project2的b+树实现确实只存了rid,然后我们通过rid就能知道实际的物理位置了
-
通过b+树组织索引结构,索引结点中存的是RID,RID可以用来指示tuple的物理位置,于是我们通过RID就可以获取到tuple,从而减少了磁盘IO。RID结构如下:
-// RID: Record Identifier |
并且,bustub保证了对于有索引的表,是不会有重复元组的,故而b+树实际上应该是一个稠密索引。
-(毕竟这个情况似乎有点复杂……物理存储上应该是按插入顺序顺序存储的,故而重复元组可能不放在一起,而我们实现的b+树又不支持重复结点,所以就会g。如果想要支持重复元组,可能就需要从两个改变思路入手,要么是修改b+树支持重复索引结点,此时b+树依然为稠密索引;要么是修改为链式存储结构以支持重复元组放在一起,此时b+树为稀疏索引。)
-非常非常崩溃,怎么保存索引尝试了很久都没做到:
-// 这样不行…… |
必须要在把资源输入到resp的stream前设置好,精确来说是调用sos.write
前设置好,不然无法起作用。
猜测是因为可能resp会根据disposition方式的不同而自动决策write的方式。
+
|
没办法,最终只能保存tree,iterator在next里动态获取了,我真是服了。等之后看完c++primer或者c++水平有所提升了再来解决这个问题吧。
-难绷,本来以为这个index-scan应该是最简单的,毕竟只用调用现成索引接口,没想到居然写了最久,可能足足两三个小时。。。
-首先的一个大难点就是如何保存迭代器了。在之前的seq-scan的时候,使用的是unique ptr,然而这里却不行会报一堆奇奇怪怪的错误(具体见一些想法-c++知识
)。最后只能换一个思路,不保存迭代器而是保存next_key_
了。然而又由于之前b+树的实现bug问题,导致对end iterator解引用是合法的,所以会产生各种奇奇怪怪的错误。解决了这个之后,之前写的insert、update、delete的更新索引部分又出了问题,rid和insert_key弄错了,诸如此类。
总之,解决了这一大堆小问题之后,才总算通过了index-scan的测试,真是令人南蚌。具体改了什么bug可以详情见b8d3ba546cfdea6fc576ad8d668322c87f6386c1
这个commit。
同时,也跟上面的sequence scan一样,都需要对标识为deleted的元组进行跳过处理。
---这里我也是没想太多……事实上,index scan无需实时检测is_deleted字段并做处理,因为索引是会随着修改实时更新的,被删除的tuple不会在索引中。
-
--In this task you will add an aggregation executor, several join executors, and enable the optimizer to select between a nested loop join and hash join when planning a query.
-You will complete your implementation in the following files:
+会话
会话:一次会话中包含多次请求和响应。
-
- -
src/execution/aggregation_executor.cpp
- -
src/execution/nested_loop_join_executor.cpp
- -
src/execution/hash_join_executor.cpp
- +
src/optimizer/nlj_as_hash_join.cpp
- +
一次会话:浏览器第一次给服务器资源发送请求,会话建立,直到有一方断开为止
+- +
功能:请求之间本来是相互独立的。将多次请求组织在一次会话中,就可以让请求之间进行数据的共享。
+方式:
++
-- +
客户端会话技术 Cookie
+把数据存进客户端
+服务器端会话技术 Session
+把数据存进服务器端
+
--The
-AggregationPlanNode
is used to support queries like the following:- -
EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA;
EXPLAIN SELECT COUNT(colA), mi(colB) FROM __mock_table_1;
EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA HAVING MAX(colB) > 10;
EXPLAIN SELECT DISTINCT colA, colB FROM __mock_table_1;也即聚合函数和DISTINCT、GROUP这种。
-
此处注意DINSTINCT也是通过aggregation实现的:
-EXPLAIN SELECT DISTINCT colA, colB FROM __mock_table_1; |
客户端会话技术,将数据保存到客户端
+使用步骤:
+代码
+
|
-The aggregation executor computes an aggregation function for each group of input. 作用于每一组
-It has exactly one child. The output schema consists of the group-by columns followed by the aggregation columns.
-+
EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA;
=== OPTIMIZER ===
// types标志聚合的种类,aggregates标识聚合的目标,group_by单独用于表示是否有group
Agg { types=[min], aggregates=[#0.1], group_by=[#0.0] } | (__mock_table_1.colA:INTEGER, <unnamed>:INTEGER)
MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
EXPLAIN SELECT COUNT(colA), min(colB) FROM __mock_table_1
=== OPTIMIZER === // 如果没有group,则其字段为空
Agg { types=[count, min], aggregates=[#0.0, #0.1], group_by=[] } | (<unnamed>:INTEGER, <unnamed>:INTEGER)
MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
public class ServletDemo2 extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
System.out.println(request.getCookies());
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}
得到效果
+运行服务器,首先访问/demo,然后在同一个浏览器再次访问/demo2,就可以在控制台看到输出。
+这个过程发生了什么呢?
+首先,访问/demo就相当于建立了会话。/demo的Servlet获取到请求之后,在response中将cookie填入。
+保持浏览器窗口不变,会话也不变。
+再次访问/demo2,cookie信息自动保存在request对象中。/demo2的Servlet获取到请求之后,在控制台中打印输出了cookie。
+你看它那个API叫add,就知道数据结构差不多是个list,所以多次add就行。
+默认情况下,浏览器关闭则cookie就马上被销毁。
+如果需要持久化存储:
+cookie.setMaxAge(int seconds) |
As discussed in class, a common strategy for implementing aggregation is to use a hash table, with the group-by columns as the key.
-In this project, you may assume that the aggregation hash table fits in memory. This means that you do not need to implement a multi-stage, partition-based strategy, and the hash table does not need to be backed by buffer pool pages.
-也就是说这里采取的是基于hashtable的实现而非基于归并排序的,并且为了简单起见将hash table保存在内存中,所以无需进行多趟划分扫描。
-We provide a SimpleAggregationHashTable
data structure that exposes an in-memory hash table (std::unordered_map
) but with an interface designed for computing aggregations. This class also exposes an SimpleAggregationHashTable::Iterator
type that can be used to iterate through the hash table. You will need to complete the CombineAggregateValues
function for this class.
--Note: The aggregation executor itself won’t need to handle the
-HAVING
predicate. The planner will plan aggregations with aHAVING
clause as anAggregationPlanNode
followed by aFilterPlanNode
.
--Hint: In the context of a query plan, aggregations are pipeline breakers. This may influence the way that you use the
-AggregationExecutor::Init()
andAggregationExecutor::Next()
functions in your implementation. Carefully decide whether the build phase of the aggregation should be performed inAggregationExecutor::Init()
orAggregationExecutor::Next()
.
值得注意的是,这里的实现将COUNT(*)
和COUNT(colum)
区分开了:
enum class AggregationType { CountStarAggregate, CountAggregate }; |
参数:
+在tomcat 8 之前 cookie中不能直接存储中文数据,需要将中文数据转码——一般采用URL编码(%E3)
+在tomcat 8 之后,cookie支持中文数据。
+假设在一个tomcat服务器中,部署了多个web项目,那么在这些web项目中cookie能不能共享?
+默认情况下cookie不能共享
+共享方法:
+setPath(String path)
:设置cookie的获取范围。默认情况下,设置当前的虚拟目录
如果要共享,则可以将path设置为”/“
+不同的tomcat服务器间cookie共享问题?
+比如说:
+因为这两者似乎语义上是有区别的,大概体现为以下几点:
+setDomain(String path)
:如果设置一级域名相同,那么多个服务器之间cookie可以共享
(setDomain(".baidu.com")
,那么tieba.baidu.com和news.baidu.com中cookie可以共享)
特点:
cookie存储数据在客户端浏览器
+因而它相对不安全
+浏览器对于单个cookie 的大小有限制(4kb) 以及 对同一个域名下的总cookie数量也有限制(20个)
+关于hashtable实现聚合的相关原理及相关示例,具体可见 这篇文章。感觉这系列文章都写得挺好的,如对TiDB有兴趣可以细看。
---在 SQL 中,聚合操作对一组值执行计算,并返回单个值。TiDB 实现了 2 种聚合算法:Hash Aggregation 和 Stream Aggregation。
-在 Hash Aggregate 的计算过程中,我们需要维护一个 Hash 表,Hash 表的键为聚合计算的
-Group-By
列,值为聚合函数的中间结果sum
和count
。计算过程中,只需要根据每行输入数据计算出键,在 Hash 表中找到对应值进行更新即可。输入数据输入完后,扫描 Hash 表并计算,便可以得到最终结果。
-
故而思路也是很清晰了。我们在aggregation的实现中要做的,就是把child executor逐行喂给hashtable,最后再遍历hashtable得到结果即可。故而,我们重点需要实现hashtable的InsertCombine
函数和hashtable的iterator。
理解了hash-aggregation的算法原理后,代码逻辑方面就不算难了,其余最主要的难点应该是空值的处理。
-总结一下,bustub对空值的处理大概有以下几个要点:
+作用:
聚合函数对空值处理
-COUNT(*)
:计入空值
COUNT/MAX/MIN/SUM(v1)
:跳过空值
cookie一般用于存出少量的不太敏感的数据
空值自身运算性质
-任意运算若有一个操作数为空,那么结果也为空。
-故而,当没有使用group by
关键字的时候(也即hashtable的key为空),此时不能天真地传入一个空的AggregationKey,而应该给它随便塞某个值。不然的话,hashtable内部的比较函数在处理空值的时候恒返回false,会导致检索失败。
在不登录的情况下,完成服务器对客户端的身份识别
+比如说,以不登录情况下对某个网页进行属性设置,你下次打开的时候属性设置依然在,这是因为你的属性设置的cookie在设置后被存入到你的电脑中,下次访问该网页发出请求,服务器端就能根据请求中cookie里的属性设置信息来做出响应了。
空表情况处理
-当表为空的时候,要求:
-select COUNT(*), MAX(v1), COUNT(v1) from table_; |
这个操作我着实不懂为什么。。。所以我最终代码只能面向测试用例:
-if (!has_next && plan_->GetGroupBys().empty()) { |
--The DBMS will use
-NestedLoopJoinPlanNode
for all join operations, by default.You will need to implement an inner join and left join for the
-NestedLoopJoinExecutor
using the simple nested loop join algorithm from class.The output schema of this operator is all columns from the left table followed by all columns from the right table.
-For each tuple in the outer table, consider each tuple in the inner table and emit an output tuple if the join predicate is satisfied.
-
也即嵌套循环实现的join,与在课上学的sort merge join一样,都是古法join实现。
-nested join的实现相比之前的思路确实会复杂一些。我们需要学习如何迭代地调用Next
来实现一次嵌套循环。思路大概是这样:
Init(): |
需求:
1. 访问一个Servlet,如果是第一次访问,则提示:您好,欢迎您首次访问。
2. 如果不是第一次访问,则提示:欢迎回来,您上次访问时间为:显示时间字符串
public class ServletDemo extends HttpServlet { |
然而其中有这几个细节需要进行处理:
+服务器端会话技术,在一次会话的多次请求间共享数据,将数据保存在服务器端的对象中。HttpSession
比如说购物网站的购物车这种,就会存在session。想想也是(
左连接的实现
-需要增加逻辑:当right遍历完之后,current_left_tuple_
仍未被组装进结果过,此时需要帮其拼接上空right tuple。
session用于存储一次会话的多次请求的数据,存在服务器端
+比如说,当我们做重定向的时候,就可以选择用session共享数据(会话域)而非使用ServletContext
(此范围过大)
空表情况
-这个分支中:
-else: |
不能这样:
-else: |
这是为了防止空表情况,使得Move right一直返回false,导致之后checkPredict报空指针异常。
+session可以存储任意类型,任意大小的数据
测试要求left->Next()
调用次数与right->Init()
调用次数相同。
-+这是为了强制让NestedLoopJoin的实现不是Pipeline Break,从而导致它性能垃圾了
-
session与Cookie的区别:
+获取HttpSession
对象:
HttpSession session = request.getSession();
使用HttpSession
对象:
Object getAttribute(String name) |
客户端不关闭,服务器关闭后,两次获取的session是同一个吗?
--The DBMS can use
-HashJoinPlanNode
if a query contains a join with a conjunction of equi-conditions between two columns (equi-conditions are seperated byAND
).也就是说,当连接条件为一/多个列相等时,就可以用hash join。可以看到这是类似等值连接。
-- -
EXPLAIN SELECT * FROM __mock_table_1, __mock_table_3 WHERE colA = colE;
EXPLAIN SELECT * FROM __mock_table_1 INNER JOIN __mock_table_3 ON colA = colE;
EXPLAIN SELECT * FROM __mock_table_1 LEFT OUTER JOIN __mock_table_3 ON colA = colE;
EXPLAIN SELECT * FROM test_1 t1, test_2 t2 WHERE t1.colA = t2.colA AND t1.colB = t2.colC;
EXPLAIN SELECT * FROM test_1 t1 INNER JOIN test_2 t2 on t1.colA = t2.colA AND t2.colC = t1.colB;
EXPLAIN SELECT * FROM test_1 t1 LEFT OUTER JOIN test_2 t2 on t2.colA = t1.colA AND t2.colC = t1.colB;You will need to implement the inner join and left join for
-HashJoinExecutor
using the hash join algorithm from class.The output schema of this operator is all columns from the left table followed by all columns from the right table.
-As with aggregation, you may assume that the hash table used by the join fits entirely in memory.
-Hint: Your implementation should correctly handle the case where multiple tuples have hash collisions (on either side of the join). 必须正确处理哈希冲突的情况
-Hint: You will want to make use of the join key accessors functions
-GetLeftJoinKey()
andGetRightJoinKey()
in theHashJoinPlanNode
to construct the join keys for the left and right sides of the join, respectively.Hint: You will need a way to hash a tuple with multiple attributes in order to construct a unique key. As a starting point, take a look at how the
-SimpleAggregationHashTable
in theAggregationExecutor
implements this functionality. 可以参考SimpleAggregationHashTable
的实现Hint: As with aggregation, the build side of a hash join is a pipeline breaker. You should again consider whether the build phase of the hash join should be performed in
-HashJoinExecutor::Init()
orHashJoinExecutor::Next()
.
具体什么是hash join,可以参考 这篇文章。
-其大概思路也很简单,hash table就是一个map<key, vector<value>>
这样的数据结构,然后将两个输入的关系选举出一个小表作为Build(建立hash table),另一个作为Probe(扫描,并根据hash table->second进行迭代组合)。它其实就是一个精确了范围的nested loop join的变种,将nested里层的针对整个关系的大循环缩小为针对hash table一个bucket的小循环。
具体到这里,思路可以是这样的。首先为了简单起见,我们就不进行选举小表的判断了,固定将right child作为Build,left child作为Probe。建表的话,我们就简单粗暴地遍历right table,然后以right_key_expressions_
为keyTuple
为value直接建表(反正也是in-memory即可。。。)。然后之后,就仿照之前思路即可。
--Hash joins usually yield better performance than nested loop joins. You should modify the optimizer to transform a
-NestedLoopJoinPlanNode
into aHashJoinPlanNode
when it is possible to use a hash join.Specifically, the hash join algorithm can be used when a join predicate is a conjunction of equi-conditions between two columns. For the purpose of this project, handling a single equi-condition, and also two equi-conditions connected by
-AND
, will earn full credit.Consider the following example:
-- -
bustub> EXPLAIN (o) SELECT * FROM test_1 t1, test_2 t2 WHERE t1.colA = t2.colA AND t1.colB = t2.colC;Without applying the
-NLJAsHashJoin
optimizer rule, the plan may look like:- -
NestedLoopJoin { type=Inner, predicate=((#0.0=#1.0)and(#0.1=#1.2)) }
SeqScan { table=test_1 }
SeqScan { table=test_2 }After applying the
-NLJAsHashJoin
optimizer rule, the left and right join key expressions will be extracted from the single join predicate in theNestedLoopJoinPlanNode
. The resulting plan will look like:- -
HashJoin { type=Inner, left_key=[#0.0, #0.1], right_key=[#0.0, #0.2] }
SeqScan { table=test_1 }
SeqScan { table=test_2 }Note: Please check the Optimizer Rule Implementation Guide section for details on implementing an optimizer rule.
-Hint: Make sure to check which table the column belongs to for each side of the equi-condition. It is possible that the column from outer table is on the right side of the equi-condition. You may find
-ColumnValueExpression::GetTupleIdx
helpful.Hint: The order to apply optimizer rules matters. For example, you want to optimize NestedLoopJoin into HashJoin after filters and NestedLoopJoin have merged. 这个感觉可能意思就是说优化规则的优先级之类的吧,这里用了个例子说hash join的优先级一般得在filter和nested loop join合并了之后。
-Hint: At this point, you should pass SQLLogicTests - #14 to #15.
-
--The BusTub optimizer is a rule-based optimizer. Most optimizer rules construct optimized plans in a bottom-up way(自底向上). Because the query plan has this tree structure, before applying the optimizer rules to the current plan node, you want to first recursively apply the rules to its children. 基于规则的优化器,规则都是对语法树自底向上实施。感觉跟课内学的差不多。
-In the public BusTub repository, we already provide the implementation of several optimizer rules. Please take a look at them as reference.
-
在课程中学到的语法优化,应该也是基于规则的优化,具体见下图及之后列出的无穷无尽个定理:
-(本图新增了一条规则:选择+嵌套笛卡尔积=嵌套连接)
-查看目录src/optimizer/
,我们可以看到:
$ tree ../src/optimizer/ |
在本小节任务中,我们需要做的,就是参照其他的规则来实现nlj_as_hash_join
。但在此之前,我们不妨先研究一下它语法优化的总体架构。
auto Optimizer::OptimizeCustom(const AbstractPlanNodeRef &plan) -> AbstractPlanNodeRef { |
可以看到,它的实际原理很简单,就是按照这样的优先级顺序对语法树运用规则进行优化。
-以OptimizeMergeFilterNLJ
为例,我们可以研究一下它的整体架构:
auto Optimizer::OptimizeMergeFilterNLJ(const AbstractPlanNodeRef &plan) -> AbstractPlanNodeRef { |
可见,对语法树运用该merge filter nlj规则是采用自底向上的顺序,并且仅合并那些filter-笛卡尔积的结点。那么接下来,我们可以具体关注RewriteExpressionForJoin
的实现。
首先,我们需要明确bustub中对expression的抽象。以#0.0=#1.0
为例,expression的结构树如下所示:
每个叶子结点都是一个基本的expression类型,如column value、constant value等等等,整个子树构成一个其他expression类型,如comparation expr、arithmetic expr等等等。
-在未优化前,我们是先做笛卡尔积,再做选择。故而,假设t1有2列,t2有2列,选择条件为t1.col1 = t2.col4
,在未优化前,filter结点的expr将为:#0.0=#0.3
(两表经过笛卡尔积合在一起了)。故而,在RewriteExpressionForJoin
函数中,我们需要根据t1表和t2表分别的列数,将#0.0=#0.3
这样的表达式转化为#0.0=#1.1
这样的表达式(其实也就是只用处理所有类型为colum expr的叶结点即可)。而由于expression是递归结构,所以我们需要先针对其所有子节点进行处理。故而,RewriteExpressionForJoin
的实现如下:
auto Optimizer::RewriteExpressionForJoin(const AbstractExpressionRef &expr, size_t left_column_cnt, size_t right_column_cnt) -> AbstractExpressionRef { |
--Specifically, the hash join algorithm can be used when a join predicate is a conjunction of equi-conditions between two columns. For the purpose of this project, handling a single equi-condition, and also two equi-conditions connected by
-AND
, will earn full credit.
看完了merge filter nlj的实现之后,本次任务的实现就变得不那么困难了。
-当一个nlj的predicate条件是一堆使用AND连接的“=”expr,我们就可以将该nlj转化为hash join。而OptimizeNLJAsHashJoin
作用于OptimizeMergeFilterNLJ
之后,故而,我们可以直接对所有的nlj结点进行判定重写。
具体来说,我们可以首先实现一个函数CheckIfEquiConjunction
,给定expr结构树输入,判断其是否只由AND、”=”、”column expr”构成。这个过程还需要做一件事,就是分离出hash join所需要的key expression,如nlj的连接条件为#0.1=#1.2 AND #1.1=#0.2
,则最后形成的hash join为left_key_expr=[#0.1, #0.2], right_key_expr=[#1.2, #1.1]
。
然后,在OptimizeNLJAsHashJoin
函数主体中,我们只需遍历语法树的所有结点,然后对其进行判定,符合条件则将其转化为hash join即可。
--You will finally implement a few more common executors, completing your implementation in the following files:
-
- -
src/execution/sort_executor.cpp
- -
src/execution/limit_executor.cpp
- -
src/execution/topn_executor.cpp
- +
src/optimizer/sort_limit_as_topn.cpp
- 不是同一个,但是要确保数据不丢失。tomcat自动(IDEA不会活化)完成以下工作
+
-- session的钝化:(序列化)
+* 在服务器正常关闭之前,将session对象序列化到硬盘上 +
You must implement the
-IndexScanExecutor
in Task #1 before starting this task. If there is an index over a table, the query processing layer will automatically pick it for sorting. In other cases, you will need a special sort executor to do this.For all order by clauses, we assume every sort key will only appear once. You do not need to worry about ties in sorting.
-
--If a query’s
-ORDER BY
attributes don’t match the keys of an index, BusTub will produce aSortPlanNode
for queries such as:- -
EXPLAIN SELECT * FROM __mock_table_1 ORDER BY colA ASC, colB DESC;如果要求排序的key不是index key,就会用到这个sort executor。
-This plan node has the same output scheme as its input schema. You can extract sort keys from
-order_bys
, and then usestd::sort
with a custom comparator to sort the child node’s tuples. You may assume that all entries in a table will fit entirely in memory.If the query does not include a sort direction (i.e.,
-ASC
,DESC
), then the sort mode will bedefault
(which isASC
).
--我有一个类Tuple,另一个类Executor。我想实现一个Tuple的比较函数,但需要用到类Executor的成员变量,那么我该怎么写一个可以用于std::sort的cmp函数
-
最终给出的提示是这样的,实现一个函数对象。
-struct CompareTuplesByOrder { |
* 具体是会放在这里:
-可以看到,其本质是通过重载”()”运算符来实现的,感觉是一个很有意思的trick。
-实现
它提示的实现思路很简单,就是大概从sort plan node获取所有key,然后用std:sort
即可,默认升序,并且所有entry都是in-memory的。
-有一点值得注意的是,在sql语言中,排序是可以指定多个关键词+不同顺序(关键词出现顺序表明排序优先级)的,如order by col1 ASC, col3 DESC
。所以我们需要在comparator实现中按照优先级(也即order_by_
数组顺序)一步步比较。
-比较难的地方大概还是c++知识,也即如何为std:sort
实现一个较为复杂的comparator。具体操作可见一些想法-comparator实现
。
-Limit
-The LimitPlanNode
specifies the number of tuples that query will generate. Consider the following example:
-EXPLAIN SELECT * FROM __mock_table_1 LIMIT 10;
+ ![image-20230223104447097](./JavaWeb/image-20230223104447097.png)
+
+* session的活化:(反序列化
+ * 在服务器启动后,将session文件转化为内存中的session对象即可。
-The LimitExecutor
constrains the number of output tuples from its child executor. If the number of tuples produced by its child executor is less than the limit specified in the plan node, this executor has no effect and yields all of the tuples that it receives.
-This plan node has the same output scheme as its input schema. You do not need to support offsets.
+我想,cookie应该在这点上不会像session这么做,因为cookie本质上是保存在客户端的数据,按理来说服务器端把cookie发出去之后就可以销毁了,在服务器序列化一点意义都没有。
+
+销毁时间
+服务器关闭
+session对象调用invalidate() 。
+session默认失效时间 30分钟
选择性配置修改
可以在每个项目的子配置文件(如下图)或者总的项目的父配置文件apache-tomcat-8.5.83\conf\web.xml
中配置
<session-config> |
+-需求:
++
- 访问带有验证码的登录页面login.jsp
+- 用户输入用户名,密码以及验证码。
++
+- 如果用户名和密码输入有误,跳转登录页面,提示:用户名或密码错误
+- 如果验证码输入有误,跳转登录页面,提示:验证码错误
+- 如果全部输入正确,则跳转到主页success.jsp,显示:用户名,欢迎您
+
挺简单的,就是限制输出的数量,没什么好说的。
---Finally, you should modify BusTub’s optimizer to efficiently support top-N queries. (These were called top-K queries in class.) Consider the following:
-+
EXPLAIN SELECT * FROM __mock_table_1 ORDER BY colA LIMIT 10;初见思路
我们可以在服务器端使用session存储password和username的map,存储验证码图片编号和图片的map,然后用cookie携带验证码图片编号,在req中存储用户名和密码。
+正确思路
感觉我上面的思路是没有充分利用到session的性质,仅仅把它作为在服务器端存储数据的工具,
+“在服务器端存储password和username的map,存储验证码图片编号和图片的map,然后用cookie携带验证码图片编号,在req中存储用户名和密码。”
+这样也依然成立,跟session没半毛钱关系。我们可以这样使用session:
++
+- 在服务器端存储password和username的map,存储验证码图片编号和图片的map
+- 当会话建立,由于没有cookie,故而session第一次创建。我们在session内写入验证码对应的编号,把图片通过response发送给客户端。
+- 会话端输入图片验证码后,按下submit按键,验证码存入request域,向服务器端发送请求
+- 服务器端Servlet从请求中get到验证码,然后在session中get到当前验证码的图片编号,向一开始存储的map查询数据,这样就能验证验证码是否正确了
+那么在这里为什么不用Cookie而使用session呢?大概是因为cookie不安全罢(慌乱)
+代码
jsp
-
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/practice_war/loginServlet" method="post">
name: <input type="text" name="username" id="username" placeholder="请输入用户名">
password: <input type="password" name="password" id="password" placeholder="请输入密码">
verifycode:<input type="text" name="verifycode" id="verifycode" placeholder="请输入验证码">
<img id="img" src="/practice_war/check"/>
<input type="submit" value="submit">
</form>
<script>
window.onload = function(){
document.getElementById("img").onclick = function(){
this.src = "/practice_war/check?"+new Date().getTime();
}
}
</script>
</body>
</html>By default, BusTub will plan this query as a
-SortPlanNode
followed by aLimitPlanNode
. This is inefficient because a heap can be used to keep track of the smallest 10 elements far more efficiently than sorting the entire table.Implement the
-TopNExecutor
and modify the optimizer to use it for queries containingORDER BY
andLIMIT
clauses.An example of the optimized plan of this query:
-+
TopN { n=10, order_bys=[(Default, #0.0)]} | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)checkcode
-
public class ServletDemo extends HttpServlet {
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//验证码图片大小
final int width = 100;
final int height = 50;
/* 绘制验证码 */
BufferedImage image = new BufferedImage(width,height,BufferedImage.TYPE_INT_RGB);
Graphics pen = image.getGraphics();
//绘制背景
pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
pen.fillRect(0,0,width,height);
//绘制边框
pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
pen.drawRect(0,0,width-1,height-1);
//随机填充字母数字
String source = "QWERTYUIOPASDFGHJKLZXCVBNMqwertyuiopasdfghjklzxcvbnm1234567890";
StringBuilder verifyAnswer = new StringBuilder();
for (int i = 1; i <= 4; i++){
int index = (int)(Math.random()*source.length());
verifyAnswer = verifyAnswer.append(source.charAt(index));
pen.drawString(source.substring(index,index+1),20*i,27);
}
//画干扰色线
pen.setColor(new Color((int)(Math.random()*255),(int)(Math.random()*255),(int)(Math.random()*255)));
for (int i = 0; i < 5; i++){
pen.drawLine((int)(Math.random()*width),(int)(Math.random()*height),(int)(Math.random()*width),(int)(Math.random()*height));
}
request.getSession().setAttribute("verifycode",verifyAnswer.toString());
System.out.println("verify:"+verifyAnswer.toString());
//将图片输出
ImageIO.write(image,"jpg",response.getOutputStream());
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
this.doGet(request, response);
}
}Hint: See
-OptimizeSortLimitAsTopN
for more information, and check the Optimizer Rule Implementation Guide for details on implementing an optimizer rule.Note: At this point, your implementation should pass SQLLogicTests #16 to #19. Integration-test-2 requires you to use release mode to run.
-
感觉标准做法应该是使用快速排序的partion思想。但是这里的话,我打算使用另一个实现更简单的思路,也即维护一个元素个数为limit_
的有序set,每次插入元素同其最小值比较即可,这样的时间复杂度为O(nlogk)
。当k不会太大的时候,我觉得这样应该还是会比快排要快些的。
本来写到这准备开开心心提交了,突然发现自己的版本似乎跟仓库最新不大一样。rebase了感觉一下午(最后甚至还找了个以前写的小bug……),最后才终于提交完获得了full score……
-bustub仓库中的每个课程版本都是有这样的小tag了,一开始没发现直接大力出奇迹rebase最新,结果整了半天人麻了。。。
-TODO
-]]>idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/
|
这一段写得很好,非常易懂地概括了什么是“多线程把异步转化为同步”:把异步中的不同操作分解为一个个独立的同类型操作,然后只需实现这些相较简单的同类型操作,再异步地把它们调度起来就行。线程正是把复杂的异步工作流分解成了一组简单的同步工作流。
-如果一个模块在代码中引入了并发性,那么它所有的代码路径【调用链】都得是并发的。
-老师的写法是将错误信息直接写在原登录界面,和我的略有不同:
+// in loginServlet |
最后一句话很关键,“把线程安全性封装在共享对象内部”
-// in login.jsp |
以及success.jsp
仅以成功为例
+
|
++现在都用 Thymeleaf ,更符合 MVC 的执行过程,也没有 JSP 这种耦合杂乱的页面代码,但是模板引擎的思路大致相同,还是可以看一看的
+
改动之后无需重启服务器,刷新界面即可。
+++关于热更新的机制可以看看这篇文章,水平有限还看不懂就先放在这了:
+ +
JSP(Java Server Pages) Java服务器端页面,用于简化书写
+可以理解为:一个特殊的页面,其中既可以定义html标签,又可以定义java代码
+比如说,上一个案例的Servlet代码就可以直接写入到JSP中,而且response和request这些对象可以直接用
+<%@ page import="java.text.SimpleDateFormat" %> |
最终效果:
+这个不同于上面的方法:将共享对象包装为线程安全的。它是要求了这些共享对象仅能在事件线程中运行,这样来保证线程安全性。
-**线程安全的核心就是对状态的访问和操作进行管理**,特别是对那些共享(shared)的、可变(mutable)的状态。关于本句话,其中几点将在下面一一细说:
+JSP本质上是Servlet
+JSP定义Java代码的方式
状态
-状态是指存储在状态变量里的数据,如成员变量、静态域等等等。对象的状态还可能包括其他依赖对象的域,如HashMap的状态包括Map.Entry的状态。
+<% 代码 %>
+定义的java代码,在service方法中。service方法中可以定义什么,该脚本中就可以定义什么。
+也即最后会构成Servlet体
共享和可变
-共享意味着变量可以由多个线程同时访问,可变意味着变量的值在生命周期可发生变化
+<%! 代码 %>
+定义的java代码,在jsp转换后的java类的成员位置。可以是成员变量,或者是成员方法。
+注:最好不要在Servlet中定义成员变量,否则容易引发线程安全问题。
是否需要线程安全
-取决于它是否被多个线程访问。比如说,如果一个局部变量仅在某个函数体中同时只被一个线程访问,那么它就不需要线程安全,不需要同步机制。
+<%= 代码 %>
+定义的java代码,会输出到页面上。输出语句中可以定义什么,该脚本中就可以定义什么。
+比如说可以用来输出某个变量的值。注意这东西由于本质上是写在Servlet的service方法中的,因而当成员变量和service方法的局部变量重名,会依据就近原则优先使用局部变量的值。
也就是jsp开头那些东西,比如说这个:
+<%@ page contentType="text/html;charset=UTF-8" language="java" %> |
用来配置jsp的资源页面信息
+contentType="text/html;charset=UTF-8" |
* <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
+ * prefix:前缀,自定义的。之后就可以用`<c:XXX>`了。相当于什么std::。
+
+但是注意一点
+//获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK |
注意,线程安全不会违背不变性和后验条件,这句话在后面会用到。
-在此举例一个无状态线程:
-
|
无状态对象一定是线程安全的
-我们可以在无状态对象的基础上为它增加一个域:
-这是线程不安全的,因为++count包含了三个动作:读取—修改—写入
-mov reg,count |
它并不具有原子性。
-在并发编程中,这种由于时序原因产生错误的情况叫做“竞态条件”。
-竞态条件有两种常见的类型。两种竞态条件的本质其实都是“基于对象之前的状态来定义对象状态的转换”。对于读取-修改-写入,是先copy原值,然后对原值+1,再写回,这是基于对象之前的状态来定义对象状态的转换;对于先检查后执行,很显然就是判断原值然后再转换到下一个状态,这就不必说了。
-如上引例
-实例:懒加载,延迟初始化中的竞态条件
-public class LazyInitRace { |
这书里讲得云里雾里的,百度了一下:
-比如说书给例子,线程向共享对象读写数据,线程是操作对象A,共享对象是被操作对象B。则:
-竞态条件:在乎的是被线程操控的共享对象的结果是否正确
-数据竞争:在乎的是操作共享对象后,线程的结果是否正确。
-确实,书里对数据竞争强调的是一个读一个写,对竞态条件更像是两个同时写
-我们可以用一个线程安全类来解决前面的Count请求的需求:
-
|
上面说到,当对象内仅有一个状态时,可以通过使用线程安全类来保障原子性。但当对象里存在多个状态时,就必须用锁来进行线程同步,而非简单地用多个线程安全类。
-还是以上面的实例来解释。
-public class UnsafeCachingFactorizer implements Servlet{ |
这段论述非常精彩,昭示了两个道理:1.分析线程安全性的时候,可以从“不变性条件不被破坏”开始考虑,首先考虑不变性条件应该是什么。2.在不变性条件涉及的多个变量彼此不独立,因而这些变量需要同时同步更新,上面那个例子就是因为不变性约束条件中的两个不独立变量没有同时同步更新。
-确实,非常重要的一点就是在两个需要连续同时修改的变量之间有了并行的时间间隔,导致此期间并行的线程的不变性被破坏。
-同步代码块包含两部分,锁的引用和保护的代码段。关键字synchronized修饰的方法就是一段同步代码段,其锁对象为当前实例【非静态方法】或者是当前class的实例【静态方法】。
---这个具体的“锁”是什么以前是真不知道。已知的是所有Object都有wait和什么什么notify方法。不过想想也确实。所有线程争抢着访问一个对象的某个同步方法段,这不正跟所有线程争抢着一个锁是差不多意思的吗?“锁”的定义其实是很宽泛的
-
java的内置锁并非无饥饿的。当线程B永远不释放锁,A会一直等待下去。
-我们可以用synchronized来解决上面的计数器问题,即直接给service方法设为synchronized。当然这种方法性能很糟糕,因为它极大降低了并发度。
-其中关于粒度的理解:
-不是“每一次调用获取一次锁,该锁属于该此调用”,而是“每个线程调用时获取一次锁,该锁属于该线程”
-public class Widget { |
比如上述代码,创建了一个LoggingWidget实例,然后调用该实例的dosmething方法,就会获取到该实例的锁。如果不允许重入,那么在做super.doSomething时,该实例的锁【注意,是同一个实例】已经被占用还未释放,因此产生死锁。有重入就可以避免此问题。
-但这很考验人的记性。一旦你在某个地方忘了同步了就会寄。
-上面那个直接对service方法进行synchronized的改善方法粒度太粗了,可以试试如下方法:
-
|
毕竟因数分解的时候无需同步保护,因为这时候参与运算的都是局部变量。
-上一章讲述了,线程安全的本质就是对共享和可变状态进行管理,以及介绍了用锁来保护状态。
-本章将引入同步除原子性外的另一特性——可见性,然后再介绍如何构建线程安全类,并且安全地发布和共享对象。
-关键词:可见性 Volatile 线程封闭 不可变对象
-public class Main { |
--关于此程序显示出的对于内存可见性的理解,可以看这篇文章:
- -- -
其实原因非常显而易见:主线程改了之后不会立刻把变量刷新到主存【可能默认是在ret时刷新,或者定时刷新,前者会导致相互等待的死锁,后者也会产生性能问题】,导致主线程的那个修改的flag变量对t1线程是**不可见**的,因此t1会继续循环等待。
-
注意,最低安全性不适用于非volatile类型的64位数据。
-要实现这种操作,我们可以设想一下关于内存可见性这一块内置锁的实现原理:lock时绑定指定变量,unlock时再刷新这个/些绑定变量的内存。
-所以说得有锁,并且锁还得是对的。
-看着看着有种always语句块的感觉了2333
-注意不放在寄存器里或者线程的私有栈里
-这样做在Servlet不会导致中文乱码,但JSP不行,这个大概是因为两者原理不一样。
+Servlet的中文乱码:
+JSP的:
+Servlet乱码是因为客户端和response请求体编码不一致,JSP乱码与JSP的原理有关,是只跟服务器端有关
--+
编译jsp有以下几个步骤:
+
(1)把jsp转化为java源码。pageEncoding=xxx指定以xxx编码格式读取jsp文件,因此,jsp文件的编码格式应与pageEncoding值一致。
(2)把java源码编译为字节码,即.class文件。转化后的java源码为utf-8编码格式,字节码也为utf-8编码,我们无需干预。
(3)执行.class文件。在此过程,需向浏览器发送中文字符,contentType=xxx指定了jsp以xxx编码显示字符。也就是在浏览器中查看页面编码,其值为contentType指定的编码。因此,在1、3环节,**只要指定一致的编码格式(jsp文件编码格式=pageEncoding=contentType)**,即可保证jsp页面不出现乱码。
举例:jsp文件以utf-8格式编写,那么pageEncoding=utf-8, contentType=utf-8,就保证了jsp页面不出现乱码。
————————————————
版权声明:本文为CSDN博主「liuhaidl」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/liuhaidl/article/details/84012372
- --但这个有争议:
-+
指定方法是在JSP开头添加:
++
<%@ page pageEncoding="UTF-8"%>内置对象
在jsp页面中不需要获取和创建,可以直接使用的对象。
+jsp一共有9个内置对象。
++
- +
request
+HttpServletRequest
一次请求访问的多个资源(转发)- +
response
+HttpServletResponse
响应对象out:
JspWriter
字符输出流对象。可以将数据输出到页面上。和response.getWriter()类似-
-- 在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
+- response.getWriter()和out.write()的区别: 在tomcat服务器真正给客户端做出响应之前,会先找response缓冲区数据,再找out缓冲区数据。因而response.getWriter()数据输出永远在out.write()之前 所以说,用out更好,因为它跟随你布局变化,你out写在哪,这句话最终就会输出在哪
也就是说,
-如果线程B在+1前知道数据无效了,就会重新载入数据然后+1然后载入内存,结果正确;
-如果线程B在+1后才知道数据无效,虽然会重新载入数据,数据为A修改后的新数据,但是此时指令无法回退,因而只能继续执行下一条指令:写回内存,B写回内存的是A修改后的新数据,因而结果错误。
-- -
大致过程:
a:mov reg 1
b:mov reg 1
a:add reg 1
b:add reg 1
a:mov reg mem
然后b线程得到通知,重新载入数据:mov reg mem
但是指令无法回退:mov reg mem
因而结果是A修改后的值被写入了两遍。所以其实volatile仅确保单次读写的瞬时线程安全。
-- -
以下是别人的理解扩展:
- - - -
下面给出一个volatile的典型用法:检查某个状态标记以判断是否退出循环。【也就是上文那个例子】
-volatile boolean asleep; |
这个“逸出作用域”的表述非常不错。
-当把一个对象传递给某个外部方法,就相当于发布了这个对象。
-外部方法:
“this escape”
-public class ThisEscape{ |
--java this 逸出_Java并发编程——this引用逸出(“this” Escape)
-并发编程实践中,this引用逃逸(“this”escape)是指对象还没有构造完成,它的this引用就被发布出去了。
-ThisEscape在构造函数中引入了一个内部类EventListener,而内部类会自动的持有其外部类(这里是ThisEscape)的this引用。source.registerListener会将内部类发布出去,从而ThisEscape.this引用也随着内部类被发布了出去。但此时ThisEscape对象还没有构造完成 —— id已被赋值为1,但name还没被赋值,仍然为null。这样一来,就有些线程持有不完整实例,不确定性太大了
-
也就是说,如果是单线程情况下,这样做是没问题的,毕竟最后都会构造完整。但多线程情况下,这俩有时间间隔,因此会产生问题,并且不能靠简单地把这句发布对象的语句放在构造函数最后一行。
-这段话非常值得注意
-所以说上面那个例子的正确代码:
-public class SafeListener { |
线程封闭一般有三种方法,这三种方法的规范性是逐级递增的。
---这里,书写得非常地抽象。通过查阅资料可得解释得更通俗的:
- -Example of ad hoc thread confinement in Java
-总之其实精华就这一句话:
-并且都是人为约束,并且一般可能会用volatile来控制单线程写这种情况下的同步。
--
// Don't modify this from any other thread than Thread X.
// So use it read-only for those other threads.
private volatile int someNumber;
也就是我们前面说的,局部变量只能在该线程内访问,除非逸出了,否则是非常安全的。
-因而需要格外注意逸出问题
-下面给出对基本类型和引用类型栈封闭的实例:
-public int loadTheArk(Collection<Animal> candidates){ |
- --
上面介绍了使用局部变量来实现线程封闭的方法,也就是栈封闭。它只要合理地控制在调用方法时不发生逸出,就可以实现线程安全。
-当有多个线程都需要同一类对象【比如Connection对象、ThreadID】,并且要求每个线程内的该对象是不一样的,并且该对象需要在多个方法中访问,栈封闭的方法就显得有些麻烦和不够优雅:需要在每个线程内都创建一个不同的对象实例,并且在调用方法的时候,都把该对象实例作为参数传进去。
-这时候就需要ThreadLocal类了。
-ThreadLocal类会给每个线程分配一个对象,并且仅需使用get方法,就能自动地把线程中的对象给弄出来。并且这些分配的对象对于各个线程来说都是隔离,相互不可见的,因此实现了线程封闭,具有安全性。
-以ThreadID为例:
-For example, the class below generates unique identifiers local to each thread. A thread’s id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls.下面代码保证每个线程首次调用ThreadId.get方法后可以分配到一个不重ID,并且ID一旦确定,之后再调用get方法得到的ID是不会改变的。它这相当于维护了一个共有的计数器局部变量。
-import java.util.concurrent.atomic.AtomicInteger; |
对于每个想获取自身ThreadID的线程,在所有想用到ID的方法中,只需:
-void method(){ |
而不用:
-AtomicInteger myID = getID(); |
这样大大简化了实现。
-再比如:
-private static final String DB_URL = ""; |
关于这个的大概代码猜想:
-这样一来,在一个线程中使用toString,就仅需造一个buf【这个是ThreadLocal封闭】,而不用每次调用都造一个【这个是栈封闭】了
-private static final ThreadLocal<char[]> buf |
我们可以初步猜想,ThreadLocal大概是通过一个map实现的,里面存储着<Thread,value>这样的键值对,每次就能通过Thread来取出对应的value了。Java低版本确实是这么做的。但Java的高版本对此进行了优化。
-从本来的:ThreadLocalMap<Thread, value> ∈ ThreadLocal
-变成了: ThreadLocalMap<ThreadLocal, value> ∈ Thread
-并且其中的ThreadLocal这个key是以弱引用【WeakReference】的方式实现的。
-- --这样的结构演进有什么好处
-
在旧版的ThreadLocal中,所有线程都将本地变量存在同一个ThreadLocalMap中,当并发量比较高的时候,ThreadLocalMap中的数据量会很大,而新版的ThreadLocalMap是属于线程的,也就是每个线程都操作属于自己的ThreadLocalMap,那么map中存储的变量就只有自己所存入的,数据量大大减少。还有一个好处,旧版的ThreadLocalMap属于ThreadLocal,当Thread实例被销毁时ThreadLocalMap里该线程的数据不会被同时销毁【这也许就带来了危险性】,而新版ThreadLocalMap属于线程,线程被销毁时,ThreadLocalMap也随之销毁。
-
--This class provides thread-local variables.线程局部变量,也就是我们说的线程封闭手法。
-These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. 每个线程都有它自己的、独立初始化的该变量的副本。
-ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).它一般用于私有静态字段,whose 状态和线程关系密切。
-Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible.
-After a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).最后会被垃圾回收
-
public class ThreadLocal<T> { |
注意点:
+ +pageContext PageContext
当前页面共享数据,还可以获取其他八个内置对象
session HttpSession
一次会话的多个请求间
application ServletContext
所有用户间共享数据
page Object
当前页面(Servlet)的对象,相当于this
config ServletConfig
Servlet的配置对象
exception Throwable
异常对象。只在page指令的isErrorPage
为true的情况下才能使用此对象。
其中,
+这四个为用来共享数据的域对象
+将程序分成三个部分,分别是M-V-C。
存在哈希冲突的话,大概是采用的线性探测方法。
+关于其remove方法:
-public void remove() { |
两篇文章都有解释
---remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。
-实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
-所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
-ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。
-
————————————————
版权声明:本文为CSDN博主「倔强的不服」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u010445301/article/details/111322569
-+ThreadLocal内存泄漏问题的解析。
-
前面我们说到它虽然线程安全,但是它存在一个问题那就是内存泄漏。首先我们要明白为什么会内存泄漏,前面也说了ThreaLocal是一个弱引用,什么是弱引用就是当它为null时候,就会被垃圾回收机制给带走,重点就是,如果我们的ThreadLocal突然为null,然后就被回收了,但此时我们的ThreadLocalMap它的生命周期是和Thread相同的,简单理解就是,裤子没了,兜还在,兜里面还有我们的数据,这就造成了内存泄漏。
-如何解决那:我们必须在使用完ThreadLocal后,执行remove()方法,避免内存溢出。
-
————————————————
版权声明:本文为CSDN博主「某刘姓男子i的码农客栈」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_20783497/article/details/107980858
满足同步需求的另一种方案就是使用不可变对象。
-这个思路非常地简单粗暴:什么东西影响了,就直接让它消失。非常有意思2333
-如果某个对象在创建后不能被修改,那么它就叫不可变对象。线程安全性是不可变对象的固有属性之一。
-比如说final域只能在声明的成员域或者构造函数中初始化,两者本质上都是在构造函数中初始化的。
-并且不可变对象也更加安全。
-不可变性不等于将对象中的所有域都设置为final域,因为final类型的域可以是对可变对象的引用。【这就类似C语言中const指针】当且仅当满足下列条件,对象才是不可变的:
--对于这里注释提到的String类,它讲得有些让人迷惑。因而我查阅资料得到解说如下:
- -- -
class String{
//默认值是0
int hash;
public int hashCode() {
//将成员变量hash缓存到局部变量
int h = hash;
//这里使用的是局部变量,因此没有多线程修改的风险
if (h == 0 && value.length > 0) {
char val[] = value;
//求hashcode过程使用局部h变量防止产生静态条件
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
//把求出的hashcode缓存到局部变量,原子操作
//这里不需要考虑线程可见性的问题,
//如果其它线程未能及时看到最新修改,重新计算hash值代价也不大
hash = h;
}
return h;
}
}再回去看书中注释的描述:
-- -
这个的意思就是说,对每个线程来说,同一个字符串hashcode值都是一样的【每次计算都得到相同的结果】,所以就不会产生多个线程计算出不同值的情况,导致不同步的发生。
-- -
意思是说之所以hashcode值一样,是因为这个hashcode计算是基于不可变对象的:
-- -
private final char value[];并且重复计算性能代价可能远没有加锁的消耗来得大,因而这里仅使用了栈封闭来保证一定程度上的线程同步。
++
服务器将接收的请求给控制器处理,控制器控制model完成必要的运算,model把算出的东西返回给控制器,控制器再把数据交给视图展示,数据最终就回到了浏览器客户端。
+这就算是一个微型CPU了吧,控制器就是CU,模型就是ALU,也许客户端和视图什么的可以视为IO接口。
++
+- +
优缺点:
++
+- +
优点:
++
+- 耦合性低,方便维护,可以利于分工协作
+- 重用性高
+- +
缺点:
++
+- 使得项目架构变得复杂,对开发人员要求高
+那么,我们可以知道,jsp就只需负责数据的展示了。那怎么展示数据呢?这就需要用到jsp的几个技术了:
+EL表达式
+-注意,servlet3.0以来默认关闭el表达式解析,需要手动在page上加属性打开,详见 jsp文件中的el表达式失效问题解决
可变对象基础上构建不可变类
- -
- -
public final class ThreeStooge {
private final Set<String> stooges = new HashSet<>();
public ThreeStooge(){
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}
public boolean isStooge(String name){
return stooges.contains(name);
}
}- -
也就是说,实现的核心是保证可变对象不变即可。
-- - - -
Final域
- -
final不仅保证了引用对象的不可变,还保证了不可变对象初始化过程中的线程安全性。
-- -
所以说还是尽量多用final。
-Volatile与不可变对象提供弱原子性
也就是这个图片中所说的:
-- -
使用volatile变量来发布不可变对象,不仅可以更新保存在不可变对象中的程序状态,还可以为一组操作提供弱原子性。
-- -
前面的代码为:
-- -
public class UnsafeCachingFactorizer implements Servlet{
private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();
public void service(ServletRequest req,ServletResponse resp){
BigInteger i = extractFromRequest(req);
/*竞态条件*/
if (i.equals(lastNumber.get())) encodeIntoResponse(resp,lastFactors.get());
else{
BigInteger[] factors = factor(i);
//时间间隔
lastNumber.set(i);
lastFactors.set(factors);
encodeIntoResponse(resp,factors);
}
}
}如今,利用volatile和不可变类的相互配合,我们修改如下:
-- -
public class VolatileCachedFactorizer implements Servlet{
//从使用两个分别原子的变量,变为使用一个volatile修饰的不可变类
private volatile OneValueCache cache = new OneValueCache(null,null);
public void service(ServletRequest req,ServletResponse resp){
BigInteger i = extractFromRequest(req);
BigInteger[] factors = cache.getFactors(i);
if (factors == null){
factors = factor(i);
//直接new一个新容器,利用了final域在初始化过程中的线程安全,因而保证了原子性
//同时也用了volatile快刷新的性质,保证了可见性,当一个线程设置为新的,其他会立即看到
//妙啊,这样就非常完美地达成了线程安全性
cache = new OneValueCache(i,factors);
}
encodeIntoResponse(resp,factors);
}
}
public class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public OneValueCache(BigInteger lastNumber, BigInteger[] lastFactors) {
this.lastNumber = lastNumber;
//传递副本,保证不可变
this.lastFactors = Arrays.copyOf(lastFactors,lastFactors.length);
}
public BigInteger[] getFactors(BigInteger i){
if (lastNumber == null || !lastNumber.equals(i)){
return null;
}
else{
//传递副本,保证不可变
return Arrays.copyOf(lastFactors,lastFactors.length);
}
}
}- -
每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据,其本质就是利用不可变性消除了访问和更新多个变量的竞态条件。
-- -
因为依然满足该程序的不变性原理:factor数组中各个数字的乘积=lastNumber,也就是说容器对象的两个值都是正确对应的,因而容器对象处于一致的状态。又或者是因为volatile及时刷新,因此确保了各个线程的内存可见性。
-- - - -
安全发布
- -
现在,我们要来讲讲如何安全地对对象进行发布。
-一个不正确程序案例
- -
public Holder holder;
public void initialize(){ holder = new Holder(); }
//以下是Holder类定义
public class Holder{
private int n;
public Holder(int n){ this.n = n; }
public void assertSanity(){
if( n != n )
throw new AssertionError("This statement is false.");
}
}Holder类本身是没有问题的,这段代码出问题的原因是holder没有被正确地发布。
-关于holder为什么没有被正确地发布:
-参考文章
-分析过程
由参考文章1:
-- -
可知,new一个对象并非原子操作,并且很有可能先得到内存引用才初始化对象。
-因而,在上面那段不安全代码的语境下可分析:
-Holder的错误发布有三点如下:
首先明确,引用,和引用的对象的状态,这两个是两个需要独立考虑的方面。前者是一个指针值,后者是指针所指的数据。下面的点1仅考虑引用的更新,点2考虑了引用对象的状态更新。
-\1. 发布对象的那个线程给holder初始化之后,holder这个引用没有及时刷新到内存,因而对其他线程不可见,其他线程读到的holder引用是旧的。
-\2. 又或者,发布了holder还没初始化完毕的时候,别的进程读取到未完成初始化的holder这个引用,但这个引用指向的状态却是旧的,因为它还没完成初始化,其状态值为旧值或者默认值。【发生了上面new一个对象的指令重排】
-\3. 如果在assert方法中两次读取n发生了上面第二条,就可能会导致前后的n不唯一,抛出异常。
-由书中表述,如果将Holder转化为不可变类,那么该发布是安全的。
-- -
至于为什么,可见下个标题。
---此处插入思考:是否可以将
- -public Holder holder
修改为public final Holder holder
,或者volatile修饰,来解决上述问题呢?通过看该文章得知:
-- -
volatile和final都会禁止字段引用的对象在构造对象过程中发生指令重排,别的线程得到引用的时候构造已经完成,而不会先得到引用再完成构造,并且两个标志都可以保证可见性。
-不过继续读下去,书中给出了答案:我说的这个方法也是可行的。
-- -
我的疑问就是第二点和第三点。
-不可变对象的初始化安全性
- -
- - - -
安全发布的常用模式
- -
分别解说
-静态初始化对象引用
- -
volatile、final以及AtomicReferance保护引用
详见上面那个不正确案例最后的思考
-由锁保护的区域
这个区域除了是通过程序构造的,也可以是使用Java自带的线程安全类库。
-- - - -
事实不可变对象
安全发布可以保证发布时的线程安全。所以说你如果承诺发布后可以一直保证不可变,那就一直都是线程安全的。
-- -
- -
- - - -
对象的可变性与正确发布
- - - -
安全地共享对象
- - - -
第四章 对象的组合
- -
也就是说上面都是在讲怎么让一个对象的共享变得安全,下面我们讲怎么依据设计模式,让一个类更容易成为线程安全的
-如何设计线程安全的类
- -
- -
- -
- -
- - - -
收集同步需求
本质上是找不变性条件和后验条件
要保证不变性条件始终成立,确保后验条件符合预期。
-- -
讲了什么是不变性条件和后验条件:
-- - - -
无效的状态转换只能出现在原子序列中
-- - - -
依赖状态的操作
- -
也就是说先验条件和状态域相关。
-- - - -
状态的所有权
- -
- -
666666
-- - - -
实例封闭
什么是实例封闭
- -
- -
所以需要上一章学的安全发布。
-- -
- -
//通过封闭机制保证线程安全
public class PersonSet {
//不安全
//封闭在实例内部
private final Set<Person> mySet = new HashSet<>();
//对所有代码路径加锁访问
public synchronized void addPerson(Person p) {
mySet.add(p);
}
public synchronized boolean containsPerson(Person p){
return mySet.contains(p);
}
}- -
- -
阅读源码可知:
-- -
static class SynchronizedCollection<E> implements Collection<E>, Serializable {
private static final long serialVersionUID = 3053995032091335093L;
final Collection<E> c; // Backing Collection
final Object mutex; // Object on which to synchronize
SynchronizedCollection(Collection<E> c) {
this.c = Objects.requireNonNull(c);
mutex = this;
}
SynchronizedCollection(Collection<E> c, Object mutex) {
this.c = Objects.requireNonNull(c);
this.mutex = Objects.requireNonNull(mutex);
}
public int size() {
synchronized (mutex) {return c.size();}
}
public boolean isEmpty() {
synchronized (mutex) {return c.isEmpty();}
}
public boolean contains(Object o) {
synchronized (mutex) {return c.contains(o);}
}
public Object[] toArray() {
synchronized (mutex) {return c.toArray();}
}
public <T> T[] toArray(T[] a) {
synchronized (mutex) {return c.toArray(a);}
}
//注意此处没用同步
public Iterator<E> iterator() {
return c.iterator(); // Must be manually synched by user!
}
public boolean add(E e) {
synchronized (mutex) {return c.add(e);}
}
public boolean remove(Object o) {
synchronized (mutex) {return c.remove(o);}
}
public boolean containsAll(Collection<?> coll) {
synchronized (mutex) {return c.containsAll(coll);}
}
public boolean addAll(Collection<? extends E> coll) {
synchronized (mutex) {return c.addAll(coll);}
}
public boolean removeAll(Collection<?> coll) {
synchronized (mutex) {return c.removeAll(coll);}
}
public boolean retainAll(Collection<?> coll) {
synchronized (mutex) {return c.retainAll(coll);}
}
public void clear() {
synchronized (mutex) {c.clear();}
}
public String toString() {
synchronized (mutex) {return c.toString();}
}
// Override default methods in Collection
public void forEach(Consumer<? super E> consumer) {
synchronized (mutex) {c.forEach(consumer);}
}
public boolean removeIf(Predicate<? super E> filter) {
synchronized (mutex) {return c.removeIf(filter);}
}
public Spliterator<E> spliterator() {
return c.spliterator(); // Must be manually synched by user!
}
public Stream<E> stream() {
return c.stream(); // Must be manually synched by user!
}
public Stream<E> parallelStream() {
return c.parallelStream(); // Must be manually synched by user!
}
private void writeObject(ObjectOutputStream s) throws IOException {
synchronized (mutex) {s.defaultWriteObject();}
}
}就是把原来的collection给实例封闭了,之后的访问都用了同步锁。
-Java监视器模式
使用内置锁
- -
直白点来说,就是把所有要访问自己状态的地方/方法通通synchronized。
-- -
- -
//监视器模式
public final class Counter {
private long value = 0;
public synchronized long getValue(){
return value;
}
public synchronized long increment(){
if (value == Long.MAX_VALUE){
throw new IllegalStateException();
}
return ++value;
}
}这样虽然简单,但缺点就是很粗暴:同步的粒度太粗了。
-使用私有锁
也跟内置锁道理差不多
-- -
也就是说私有锁可以让外面的世界也参与到同步中来,但内置锁不大行。
-示例:车辆追踪
- -
public class MutablePoint {
public int x,y;
public MutablePoint() {
x=0;y=0;
}
public MutablePoint(MutablePoint p){
//深拷贝
this.x=p.x;
this.y=p.y;
}
}- -
//基于监视器模式
public class MonitorVehicleTracker {
private final Map<String, MutablePoint> locations;
public MonitorVehicleTracker(
Map<String,MutablePoint> locations
){
this.locations = deepCopy(locations);
}
public synchronized Map<String,MutablePoint> getLocations(){
return deepCopy(locations);
}
public synchronized MutablePoint getLocation(String id){
MutablePoint loc = locations.get(id);
//返回copy对象,深拷贝
return loc == null?null : new MutablePoint(loc);
}
public synchronized void setLocation(String id,int x,int y){
MutablePoint loc = locations.get(id);
if (loc == null) throw new IllegalArgumentException();
loc.x = x;
loc.y = y;
}
//为什么这方法不用锁?是因为调用它的地方都锁着
private static Map<String,MutablePoint> deepCopy(Map<String,MutablePoint> m){
Map<String,MutablePoint> res = new HashMap<>();
for (Map.Entry en : m.entrySet()){
//此处通过MutablePoint的构造函数重新拷贝了一个Point
//如果简单地使用HashMap的构造函数new HashMap(m)的拷贝来创建一个新的map是不行的
//因为这样只会拷贝Point对象的指针值,依然是浅拷贝
res.put((String) en.getKey(),new MutablePoint((MutablePoint) en.getValue()));
}
return Collections.unmodifiableMap(res);
}
}- - - -
将线程安全性委托给独立的状态变量
- -
定义
- -
- -
意思就是保证一个类里面仅有一个状态,只要该状态是线程安全的,那么该类也就是线程安全的。
-示例
- -
//线程安全
public class Point {
public final int x,y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
}- -
//将线程安全委托给ConcurrentMap
public class DelegatingVehicleTracker {
//用了两个
private final ConcurrentMap<String,Point> locations;
private final Map<String,Point> unmodifiableMap;
public DelegatingVehicleTracker(Map<String,Point> ps) {
locations = new ConcurrentHashMap<>(ps);
unmodifiableMap = Collections.unmodifiableMap(locations);
}
public Map<String,Point> getLocations(){
//unmodifiableMap baked by locations,所以locations变化也会反映到unmodifiableMap上
//目的只是为了提供外界无法修改的视图
//不得不说真是妙啊
return unmodifiableMap;
}
public Point getLocation(String id){
return locations.get(id);
}
public void setLocation(String id,int x,int y){
if (locations.replace(id,new Point(x,y)) == null){
throw new IllegalArgumentException();
}
}
}- -
- -
- - - -
public Map<String,Point> getLocations(){
return Collections.unmodifiableMap(new HashMap<>(locations));
}委托给多个状态变量
- -
也就是说这些对象彼此不会构成不变性条件。
-- -
//将线程安全性委托给多个状态变量
public class VisualComponent {
private final List<KeyListener> keyListeners = new CopyOnWriteArrayList<>();
private final List<MouseListener> mouseListeners = new CopyOnWriteArrayList<>();
public void addKeyListener(KeyListener listener){
keyListeners.add(listener);
}
public void addMouseListener(MouseListener listener){
mouseListeners.add(listener);
}
public void removeKeyListener(KeyListener listener){
keyListeners.remove(listener);
}
public void remiveMouseListener(MouseListener listener){
mouseListeners.remove(listener);
}
}- -
而且键盘监听和鼠标监听彼此独立。
-不独立多个状态变量不能委托
- -
public class NumberRange {
//不变性条件: lower<=upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i){
//先检查后执行
if (i>upper.get()){
throw new IllegalArgumentException();
}
lower.set(i);
}
public void setUpper(int i){
if (i<lower.get()){
throw new IllegalArgumentException();
}
upper.set(i);
}
public boolean isInRange(int i){
return (i>=lower.get() && i<=upper.get());
}
}- -
根本原因就是因为不独立。
-- - - -
发布被委托的状态变量
什么时候可以发布
- -
- -
示例
- -
- -
package sit;
public class SafePoint {
//注意此处x和y没有用任何同步修饰词修饰
private int x,y;
private SafePoint(int[] a){
this(a[0],a[1]);
}
//此处为什么不直接用this(p.x,p.y)呢?
//是因为x和y本身并没有任何线程安全的防护手段,这样做的话会发生竞态条件。况且x和y也被实例封闭了
//私有构造函数捕获模式
public SafePoint(SafePoint p){
this(p.get());
}
public SafePoint(int x,int y){
this.x=x;
this.y=y;
}
//x、y都放入数组,保证x和y的同时读写,nb
public synchronized int[] get(){
return new int[] {x,y};
}
public synchronized void set(int x,int y){
this.x=x;
this.y=y;
}
}- -
//跟上面那个委托没什么差,区别只在于上面的那个SafePoint类,既是线程安全的,又是可修改的
public class PublishingVehicleTracker {
private final Map<String,SafePoint> locations;
private final Map<String,SafePoint> unmodifiableMap;
public PublishingVehicleTracker(Map<String,SafePoint> locations){
this.locations = new ConcurrentHashMap<>(locations);
this.unmodifiableMap = Collections.unmodifiableMap(this.locations);
}
public Map<String,SafePoint> getLocations(){
return unmodifiableMap;
}
public SafePoint getLocation(String id){
return locations.get(id);
}
public void setLocations(String id,int x,int y){
if (!locations.containsKey(id))
throw new IllegalArgumentException();
locations.get(id).set(x,y);
}
}- -
- -
这仅仅是一个委托发布的实例。
-在现有的线程安全类中添加功能
引论
- -
比如说想给vector添加一个“put-if-absent”
-- -
可以用子类扩展法,也可以直接加源代码。后者有时候源代码不可访问,前者的父类很多域可能不对子类开发,并且非常脆弱。因而下面介绍几种比较好的机制。
-客户端加锁机制
定义和实例
- -
- -
public class NotThreadSafeListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<>());
public synchronized boolean putIfAbsent(E x){
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}- -
我曹,66666666
-也就是说,这里加的是ListHelper的锁,只能让别的线程不能通过putIfAbsent方法同时修改list,但别的线程完全可以直接获取list再修改。
-- -
客户端指的是我们的ListHelper。我们正是不知道list这个对象使用的是哪一个锁才发愣的。
-所以我们使用ArrayList自身的锁,也就是list自己的内置,来加锁。
-- -
//使用客户端加锁实现
public class ListHelper<E> {
public List<E> list = Collections.synchronizedList(new ArrayList<>());
public boolean putIfAbsent(E x){
synchronized(list) {
boolean absent = !list.contains(x);
if (absent)
list.add(x);
return absent;
}
}
}评价
- -
- -
它非常依赖于其他类的客户端加锁机制。
-- -
确实,毕竟你锁被外界拿去用了。
-组合
- -
- -
public class ImprovedList<T> implements List<T> {
//实例封闭
private final List<T> list;
public ImprovedList(List<T> list){this.list=list;}
public synchronized boolean putIfAbsent(T x){
boolean contains = list.contains(x);
if (contains)
list.add(x);
return !contains;
}
public synchronized void clear(){list.clear();}
//... 按照类似的方式委托List接口其他未实现的方法
}- -
是的,跟synchronizedList非常像
-- -
这也就是用的java的监视器模式了
-第五章 基础构建模块
- -
保证独立即可委托,从而构建一个线程安全类
-- -
同步容器类
- -
差不多都是使用的监视器模式。
-同步容器类:Vector、Hashtable、Collections.synchronizedXxx
-同步容器类的问题
- -
也就是需要避免两个原子操作之间的非线程安全的时间间隔。
-- -
public static Object getLast(Vector list){
//复合操作
//先检查后执行
int lastIndex = list.size()-1;
return list.get(lastIndex);
}
public static void deleteLast(Vector list){
int lastIndex = list.size()-1;
list.remove(lastIndex);
}- -
它这段话说得非常好。由于Vector这个类本身是线程安全的,因而它可以保证外部任何操作都不会导致该对象因为并发而被破坏。但是,我们用不加锁的复合操作虽然不会破坏Vector,但可能导致不能出现我们想要的结果。
-所以我们必须用锁机制来对此复合操作进行保护:
-- -
public static Object getLast(Vector list){
synchronized (list) {
//复合操作
//先检查后执行
int lastIndex = list.size()-1;
return list.get(lastIndex);
}
}
public static void deleteLast(Vector list){
synchronized (list){
int lastIndex = list.size()-1;
list.remove(lastIndex);
}
}除此之外,迭代也是一种经典的复合操作。我们可以通过下面这种粗粒度加锁来避免:
-- - - -
public static void travel(Vector list){
synchronized (list) {
for (int i=0;i<list.size();i++){
//do something
}
}
}迭代器与ConcurrentModificationException
- -
所以才会引入fail-fast机制。
-- -
--foreach语法糖内部是通过Iterator来实现的。
- --
public void testIterableForEach() {
List<String> list = new ArrayList<>();
for (String str : list) {
System.out.println(str);
}
}
//反编译后:
public void testIterableForEach() {
List<String> list = new ArrayList<>();
Iterator i = list.iterator();
while(i.hashNext()){
String str = (String)i.next();
System.out.println(str);
}
}- -
- -
可见同步容器类还是有很多局限性的。
-隐藏迭代器
有时候,迭代会隐藏起来。要一个个揪出需要加锁的地方是非常麻烦的。
-- -
//隐藏在字符串连接中的迭代操作
public class HiddenIterator {
private final Set<Integer> set = new HashSet<>();
public synchronized void add(Integer i){ set.add(i); }
public synchronized void remove(Integer i){ set.remove(i); }
public void addTenThings(){
Random r = new Random();
for (int i=0; i<10; i++) {
add(r.nextInt());
}
//隐式迭代
System.out.println("DEBUG: Added ten elements to "+ set);
}
}- -
- -
- - - -
并发容器类
同步容器类的加锁太粗粒度了,导致并发性弱。因而引入并发容器类来解决问题。
-- -
ConcurrentHashMap —— HashMap
-CopyOnWriteArrayList —— List
-- -
- -
BlockingQueue
-- -
ConcurrentSkipListMap —— TreeMap
-ConcurrentSkipListSet —— TreeSet
-ConcurrentHashMap
- -
使用分段锁来细粒度加锁。
-- -
--关于ConcurrentHashMap的分段锁:ConcurrentHashMap
-JDK1.7中,ConcurrentHashMap 类所采用的正是分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自
-ReentrantLock(可重入锁)
,这个小数组名叫Segment
。JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,采用
-CAS
+synchronized
来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是数组 + 链表(当链表长度大于 8 时,链表结构转为红黑二叉树)结构。ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍!
---关于ConcurrentHashMap的弱一致性:ConcurrentHashMap的弱一致性
-get方法是弱一致的,是什么含义?可能你期望往ConcurrentHashMap底层数据结构中加入一个元素后,立马能对get可见,但ConcurrentHashMap并不能如你所愿。换句话说,put操作将一个元素加入到底层数据结构后,get可能在某段时间内还看不到这个元素,若不考虑内存模型,单从代码逻辑上来看,却是应该可以看得到的。
-- -
精确值—>估计值
-- -
- -
--关于AQS框架:重大发现,AQS加锁机制竟然跟Synchronized有惊人的相似
-在并发多线程的情况下,为了保证数据安全性,一般我们会对数据进行加锁,通常使用Synchronized或者ReentrantLock同步锁。Synchronized是基于JVM实现,而ReentrantLock是基于Java代码层面实现的,底层是继承的AQS。
-AQS全称**
-AbstractQueuedSynchronizer
**,即抽象队列同步器,是一种用来构建锁和同步器的框架。我们常见的并发锁ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier都是基于AQS实现的,所以说不懂AQS实现原理的,就不能说了解Java锁。
---并发容器类不能实现独占访问:
-类似ConcurrentHashMap的并发容器不能采用客户端加锁机制,因为并发容器没有采用synchronized内置锁而大多基于AQS框架(不是独占式的锁),所以使用客户端加锁机制来扩展并发容器的方法是不能实现的。
-所以说不能客户端加锁不是不提倡,而是真的不行【】
-所以最好还是用并发容器类替代同步容器类。
-对部分复合操作的支持
- -
客户端加锁不能使用,就只能用它提供的东西了。
-CopyOnWriteArrayList
-
Copy-On-Write
意为“写入时复制”,仅当要修改的时候,才会重新创建一次副本,实现可变性。犹记得第一次接触到这个思想是在操作系统的fork()创建子进程的原理那个地方,那可真是有些惊为天人23333- -
- -
也就是说,COWAL内部维护的base数组是事实不可变的,因而访问它的时候不需要同步。但是,我们事实上需要一个可变的并发容器,那该怎么办呢?解决方法就是每次要修改的时候,直接把base数组换成一个新的数组,就像之前某个例子一样,这样就能实现可变性了。
-与此同时,这样的方法也能保证多线程访问时的内存可见性。
-由COWAL的底层代码:
-- -
//base数组,volatile保证引用一变就可以刷新
private transient volatile Object[] array;
final Object[] getArray() {
return array;
}
final void setArray(Object[] a) {
array = a;
}
public boolean add(E e) {
//获取锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//getArray:直接返回base数组的引用
Object[] elements = getArray();
int len = elements.length;
//创建新数组再修改
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
//直接改变base数组的引用
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}可知,它保证可见性,是直接修改引用的,并且注意,对原数组的拷贝是浅拷贝的。这样一来,就既不会改变原数组的东西,也能保证可见性的更新迅速了。我的评价是牛逼爆了。
-- - - -
阻塞队列和生产者—消费者模式
基本介绍
- -
简直就是为了生产者消费者而生的
-- -
- -
这两段话说得非常本质,需要有个缓冲队列本质上就是因为处理数据速率的不同,生产者消费者也起到了解耦作用
-- -
所以说用有界队列还是更好
-BlockingQueue有多种实现。
-ArrayBlockingQueue和LinkedBlockingQueue是FIFO队列,PriorityBlockingQueue是优先队列,最后还有一个特殊的SynhronousQueue。
-- -
- - - -
实例:桌面搜索
- -
- -
//生产者
public class FileCrawler implements Runnable {
private final BlockingDeque<File> fileQueue;
private final FileFilter fileFilter;
private final File root;
public FileCrawler(BlockingDeque<File> fileQueue, FileFilter fileFilter, File root) {
this.fileQueue = fileQueue;
this.fileFilter = fileFilter;
this.root = root;
}
public void run() {
try{
crawl(root);
}catch (InterruptedException e){
//中断处理
Thread.currentThread().interrupt();
}
}
private void crawl(File root) throws InterruptedException{
File[] entries = root.listFiles(fileFilter);
if (entries!=null){
for (File entry:entries){
//递归打印目录
if (entry.isDirectory()) crawl(entry);
else if (!alreadyIndexed(entry)) fileQueue.put(entry);
}
}
}
}
//消费者
public class Indexer implements Runnable{
private final BlockingDeque<File> queue;
public Indexer(BlockingDeque<File> q){
this.queue=q;
}
public void run() {
try{
while(true){
indexFile(queue.take());
}
}catch(InterruptedException e){
Thread.currentThread().interrupt();
}
}
}- -
- -
- - - -
//启动生产者-消费者程序
public static void startIndexing(File[] roots){
BlockingDeque<File> queue = new LinkedBlockingDeque<>(BOUND);
FileFilter filter = new FileFilter() {
public boolean accept(File pathname) {
return true;
}
};
for (File root : roots){
new Thread(new FileCrawler(queue,filter,root)).start();
}
for (int i=0; i<N_CONSUMERS;i++){
new Thread(new Indexer((queue))).start();
}
}串行线程封闭
- -
安全发布
-- -
- -
6666666
-之所以叫“串行”,想必是因为这个过程:发布-转接-放弃访问权是一个串行过程。
-- - - -
总而言之,串行线程封闭的具体做法就是,一个线程将一个安全发布的对象的所有权完全转移给另一个线程,保证之后自己不会再使用。这样一来,该对象就相当于被另一个线程封闭了。而如何保证“自己以后不再使用”呢?最简单的方法就是安全发布完这个东西后直接把这个东西给踢出去。
-阻塞队列是自动会把这个东西安全发布然后就踢出去的,所以说阻塞队列简化了这个工作。
-- - - -
双端队列与工作密取
- -
- - - -
阻塞方法与中断方法
阻塞方法
- -
当某方法抛出InterruptedException时,表明该方法为阻塞方法,也即这个方法会在执行过程中由于各种原因而被阻塞。如果这个方法被中断,它将会努力提前结束阻塞状态。
-中断方法
- -
- -
处理InterruptedException的两种选择
- -
传递InterruptedException
- -
恢复中断
- -
--此处关于为什么如果代码是Runnable的一部分就不能抛出异常:
-是因为java的异常继承体系。
-在重写的run方法中,我们只能够进行异常的捕获而不能够抛出异常,原因是因为在父类Runnable接口中,run方法没有抛出异常,则实现Runnable的子类就无法抛出异常
-所以实际上是语法层面上不允许。
-- -
- - - -
同步工具类
- -
实现同步的方法:使用同步容器类/并发容器类、使用锁、使用同步工具类
-- - - -
闭锁 CountDownLatch
作用
- -
- - - -
CountDownLatch
- -
- -
//使用CountDownLatch来进行计时测试
public class TestHarness {
public long timeTasks(int nThreads,final Runnable task)
throws InterruptedException{
//初始化计数器为1
final CountDownLatch startGate = new CountDownLatch(1);
final CountDownLatch endGate = new CountDownLatch(nThreads);
for (int i = 0; i < nThreads; i++){
Thread t = new Thread(){
public void run() {
try {
startGate.await();
try {
task.run();
} finally {
endGate.countDown();
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
};
t.start();
}
long start = System.nanoTime();
//所有线程刷拉拉往下走
startGate.countDown();
//等待所有线程结束
endGate.await();
long end = System.nanoTime();
return end-start;
}
}- -
- -
是的,这样测试出来的时间应该更加平均,性能更加准确。
-FutureTask
- -
- --关于Future:
-- -
其中get方法是阻塞的。
-获取异步任务执行完后的结果。
-关于FutureTask:
-FutureTask既包含了Future的语义,又包含了Runnable的语义。
-它其实内部封装了一个Runnable Task。调用FutureTask的run,其实本质上就是调用Task的run,只不过要多一些检查和存储结果之类的手续。
-所以说它其实就是通过内部封装一个线程,然后就能获取这个线程运行的状态和运行的结果等等等,这样来实现Future语义的。
---关于Callable
-Runnable里面的run方法是不能传参,也没有返回值的。Callable相当于有返回值的Runnable,也即书中说的“有生成结果的Runnable”。
-- -
- -
- -
public class Preloader {
private final FutureTask<ProductInfo> future =
new FutureTask<ProductInfo>(new Callable<ProductInfo>() {
public ProductInfo call() throws Exception {
return loadProductInfo();
}
});
private final Thread thread = new Thread(future);
public void start(){thread.start();}
public ProductInfo get()
throws DataLoadException,InterruptedException{
try {
//阻塞
return future.get();
} catch (ExecutionException e) {
Throwable cause = e.getCause();
if (cause instanceof DataLoadException)
throw (DataLoadException) cause;
else
throw launderThrowable(cause);
}
}
public static RuntimeException launderThrowable(Throwable t){
if (t instanceof RuntimeException)
return (RuntimeException) t;
else if (t instanceof Error) {
throw (Error) t;
}
else
throw new IllegalStateException("Not checked.",t);
}
}- -
也就是调用start线程启动后,可以就去做别的事情,回来就可以拿到结果了,通过这样实现异步调用。
-- -
- -
这里其实是在讲异常的事了。可以给我们一个启发式思路:
-Callable抛出的Exception这种抽象的异常集合该如何分解处理:首先分解出受检查的异常【也就是说我们调用该方法就知道该方法可能会抛出的异常】,然后针对其他未检查异常,再进行处理。此例中是把这些未检查异常分成了Error和RuntimeException。
-信号量 Semaphore
- -
--注意
--
这种情况下,其实信号量跟BlockingQueue语义十分近似:
-- -
信号量还可以用来将非阻塞容器包装为有界阻塞容器。
-- -
- -
- -
//使用信号量为容器设置边界,有界+阻塞
public class BoundedHashSet <T>{
//baked collection,同步容器
private final Set<T> set;
//信号量
private final Semaphore sem;
public BoundedHashSet(int bound){
//同步容器类
this.set = Collections.synchronizedSet(new HashSet<>());
//初始化许可数
sem = new Semaphore(bound);
}
//阻塞方法
public boolean add(T e) throws InterruptedException{
//获取许可
sem.acquire();
boolean wasAdded = false;
try {
//由于是同步容器类,故而不用使用锁来保护状态
wasAdded=set.add(e);
return wasAdded;
} finally {
//try捕获异常/正常return后,finally语句都会执行。
if (!wasAdded)
sem.release();
}
}
public boolean remove(Object o){
boolean wasRemoved = set.remove(o);
if (wasRemoved)
sem.release();
return wasRemoved;
}
}--注:try语句块正常return后,finally语句依然会执行:
--
public class Main{
public static void main(String[] args) {
System.out.println(haha());
}
public static int haha(){
int number = 10;
try{
System.out.println("main!");
return number++;
}finally {
System.out.println("come!"+number);
}
}
}
/*输出结果
main!
come!11
10*/栅栏 Barrier
- -
闭锁是某个事件发生后所有线程才能继续执行;栅栏是所有线程都在同样位置等待才能继续执行。
-CyclicBarrier
- -
- -
一个线程寄了,其他所有等待线程都会死。
-栅栏我觉得一个很重要的点就是保证并发安全。
-- -
常常出现那种需要等待所有线程都完成某一步操作才能进行下一步操作的情况,所以栅栏不得不说非常实用。
-- -
public class CellularAutomata {
//细胞板
private final Board mainBoard;
//栅栏
private final CyclicBarrier barrier;
//计算线程
private final Worker[] workers;
public CellularAutomata(Board board){
this.mainBoard = board;
//所有可调度的CPU都被拉过来了
int count = Runtime.getRuntime().availableProcessors();
//当所有线程到达栅栏后,马上执行该run方法:提交计算得出的新值
this.barrier = new CyclicBarrier(count,
new Runnable() {
public void run() {
mainBoard.commitNewValues();
}
});
//创建工作线程池
this.workers = new Worker[count];
for (int i = 0; i < count; i++)
//分治,把大的细胞板划分成多个小细胞板处理
workers[i] = new Worker(mainBoard,getSubBoard(count,i));
}
private class Worker implements Runnable{
private final Board board;
public Worker(Board board){this.board = board;}
public void run() {
while (!board.hasConverged()){
for (int x = 0; x < board.getMaxX(); x++)
for (int y = 0; y < board.getMaxY(); y++)
//为二维细胞板上每个点计算新值
board.setNewValue(x,y,computeValue(x,y));
try {
//计算完之后等待其它线程也计算完
barrier.await();
} catch (InterruptedException e) {
return;
} catch (BrokenBarrierException e) {
return;
}
}
}
}
public void start(){
for (int i = 0; i < workers.length; i++)
new Thread(workers[i]).start();
mainBoard.waitForConvergence();
}
}--注:
-的目的
--
Exchanger
- -
这个“写满”大概对应着栅栏思想里的“全部到达”
-- - - -
示例:构建高效且可伸缩的结果缓存
- -
- -
- - - -
public interface Computable <A,V>{
V compute(A arg) throws InterruptedException;
}
public class ExpensiveFunction implements Computable<String, BigInteger>{
public BigInteger compute(String arg) throws InterruptedException {
//经过长时间的计算后
return new BigInteger(arg);
}
}用内置锁对方法进行上锁
- -
//感受一下cache也是Computable的这个多态运用的巧妙性
public class Memoizer1<A,V> implements Computable<A,V> {
private final Map<A,V> cache = new HashMap<>();
private final Computable<A,V> c;
public Memoizer1(Computable<A,V> c){
this.c=c;
}
//对整个方法体进行上锁
public synchronized V compute(A arg) throws InterruptedException {
V result = cache.get(arg);
if (result == null){
result = c.compute(arg);
cache.put(arg,result);
}
return result;
}
}保证了线程安全,但是可伸缩性极差,而且很有可能变成普通的串行排队计算。
-- - - -
使用并发容器类
- -
public class Memoizer2 <A,V> implements Computable<A,V>{
//并发map
private final Map<A,V> cache = new ConcurrentHashMap<>();
private final Computable<A,V> c;
public Memoizer2(Computable<A,V> c){this.c = c;}
public V compute(A arg) throws InterruptedException {
V result = cache.get(arg);
if (result == null){
result = c.compute(arg);
cache.put(arg,result);
}
return result;
}
}确实加锁的粒度小了【没有把高耗时的compute过程锁上】,但会带来某个值重复计算的问题。
-- -
而且这不仅仅是性能损耗问题,还有可能会变成安全隐患。
-- - - -
使用FutureTask
- -
- -
public class Memoizer3<A,V> implements Computable<A,V> {
private final Map<A, Future<V>> cache
= new ConcurrentHashMap<>();
private final Computable<A,V> c;
public Memoizer3(Computable<A, V> c) {
this.c = c;
}
public V compute(A arg) throws InterruptedException {
Future<V> f = cache.get(arg);
if (f == null){
Callable<V> eval = new Callable<V>() {
public V call() throws Exception {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<>(eval);
f = ft;
cache.put(arg,ft);
//没有启动新线程,直接润
ft.run();
}
try {
return f.get();
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}- -
这个“判断是否开始”与“判断是否完成”的表述非常有意思。此时cache变成了arg和一个异步任务得到的未来结果的映射,而非arg和结果的映射。从此处也可好好理解感受一下“Future”的语义(一个异步执行的结果)。
-只是它依然没有解决上面所说的问题,还是可能会有两个线程计算同一个值,虽然概率小得多,主要原因是因为它使用了非原子的“先检查后执行”。
-- -
因而,我们可以通过map提供的putifabsent同步方法来解决这个问题。
-使用FutureTask和putIfAbsent
- -
public class Memoizer<A,V> implements Computable<A,V> {
private final Map<A, Future<V>> cache
= new ConcurrentHashMap<>();
private final Computable<A,V> c;
public Memoizer(Computable<A, V> c) {
this.c = c;
}
public V compute(A arg) throws InterruptedException {
Future<V> f = cache.get(arg);
//一重保险,筛选线程,防止ft对象重复声明销毁
if (f == null){
Callable<V> eval = new Callable<V>() {
public V call() throws Exception {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<>(eval);
f = cache.putIfAbsent(arg,ft);
//二重保险,保障仅一个线程能进入
//只有那个成功把ft放进map的线程才能进入。
if (f == null){
f = ft;
ft.run();
}
}
try {
return f.get();
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
}虽然这个最终版本看起来很完美,但实际上,它还会带来其他的性能问题。
-- -
- - - -
运用最终方案建立cache
- - - - - -
public class Factorizer implements Servlet{
private final Computable<BigInteger,BigInteger[]> c =
new Computable<BigInteger, BigInteger[]>() {
public BigInteger[] compute(BigInteger arg) throws InterruptedException {
return factor(arg);
}
};
private final Computable<BigInteger,BigInteger[]> cache =
new Memoizer<>(c);
public void service(ServletRequest req,ServletResponse resp){
BigInteger i = extractFromRequest(req);
try {
encodeIntoResponse(resp,cache.compute(i));
} catch (InterruptedException e) {
encodeError(resp,"factorization interrupted.");
}
}
}第六章 任务执行
- -
- -
在线程中执行任务
- -
- -
- -
也就是说,一个请求视为一个任务。这样做是非常reasonable的,因为每个请求间都是独立的。
-串行地执行任务
调度任务最简单粗暴的就是直接让任务串行执行。
-- -
//串行的web服务器
public class SingleThreadWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while(true){
Socket connection = socket.accept();
handleRequest(connection);
}
}
}- -
这里有一个点非常棒:网络也是IO,也会造成阻塞。
-为每一个任务都创建一个线程
- -
public class SingleThreadWebServer {
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while(true){
Socket connection = socket.accept();
new Thread(new Runnable() {
public void run() {
handleRequest(connection);
}
}).start();
}
}
}- -
但是其实这种方式是不好的,因为它无限制地创建线程,这听起来就很容易寄,要知道,高并发的服务器可能会一次解决几千万个请求【当然不知道有没有那么多hhh】,每个都创建一个线程的话,很容易爆内存,而且还会有很大的性能开销。
-- -
- -
- -
而且这样的话,要是高并发情况下,服务器会马上崩溃,做不到我们之前说的自我调节功能。
-所以,我们应该为系统能创建的线程数做一个限制。如此,便引出了Executor框架。
-Executor框架
- -
- -
/*
An object that executes submitted Runnable tasks.
An Executor is normally used instead of explicitly creating threads.
此接口提供了一种将任务的提交与每个任务将如何运行的机制解耦的方法,包括线程使用、调度等的详细信息。
The Executor implementations provided in this package implement ExecutorService, which is a more extensive interface. The ThreadPoolExecutor class provides an extensible thread pool implementation. The Executors class provides convenient factory methods for these Executors.
*/
public interface Executor {
void execute(Runnable command);
}- -
- -
- - - -
示例:基于Executor线程池的Web服务器
- -
public class TaskExecutionWebServer {
private static final int NTHREADS = 100;
//工厂方法创建线程池
private static final Executor exec
= Executors.newFixedThreadPool(NTHREADS);
public static void main(String[] args) throws IOException {
ServerSocket socket = new ServerSocket(80);
while (true){
final Socket connection = socket.accept();
//每次提交一个任务,自然会在某个时刻调度执行
exec.execute(new Runnable() {
public void run() {
handleRequest(connection);
}
});
}
}
}- -
- -
思考问题
Executor到底什么原理
这里感觉绕来绕去的,execute到底是会创建一个线程,还是不会创建一个线程?到底是在调用时就创建线程执行任务,还是会在将来的某一个时刻调度执行任务?它这里说得云里雾里的,我来锐评一下我的看法。
-首先,我认为,它给我们的ExecutorService类的execute应该都仅仅是提交任务,放进任务队列,之所以什么时候执行得看调度情况。【Form java ThreadPoolExecutor.execute: Executes the given task sometime in the future. 】
-而下面那两个类应该都是对execute进行了简单的重写,因而此处execute跟java包里的ExecuteService没有任何关系,调用execute仅相当于调用一个普通的方法。
-Executor到底有什么用
- --解耦
Executor作为一个接口,其核心思想便是“解耦”。
-如果没有Executor的话,我们要创建并运行一个任务,一般都得这样用:
-new Thread(....).start()
,或者是比如说串行的new Runnable(...).run()
。也就是我们将任务的创建和任务的执行都混在一起了。而假定说,如果以后要改变该线程池的执行方式,比如说从单任务单线程的并行改成全部任务都串行或者反之,那么就需要每个地方都改掉。但如果使用Executor框架将任务创建和任务具体执行解耦开来,那么我们就仅需修改任务具体执行了。Java的线程管理框架
JUC(
-java.util.concurrent
)其实就只是分为三个部分。- -
ThreadPoolExecutor
-
-- -
ExecutorService
-ThreadPoolExecutor继承了该接口。
-是Executor接口的加强版,包含了更多方法,具体为:
-① 自身生命周期的管理 shutdown、isshutdown等等
-② 对异步任务的支持 返回Future的submit方法
-③ 对批处理任务的支持 invokeall
-- -
内部原理
-当空闲的线程足够多,直接执行;当线程不够多,进入阻塞队列;当阻塞队列满,使用拒绝策略。
-内部的线程池分为救急线程和核心线程。核心线程一直存在,当阻塞队列和核心线程都不够用,就会新开几个救急线程。
-执行策略
- -
- - - -
线程池
- -
- -
- -
说得非常全面
-- -
- -
66666
-- - - -
Executor的生命周期
- -
我们结束executor,可以采取或温和或粗暴的方法:可以让它不接受新的,慢慢执行完全部再结束;也可以让它直接全部结束,管它有没有执行完或者有没有还没被执行,就跟断电一样。
-- -
- -
- -
--此处疑问:不应该先shutdown再awaitTermination吗?我百度了,也都是说先shutdown。毕竟awaitTermination方法是阻塞的。
-- -
- - - -
//支持关闭操作的Web服务器
public class LifecycleWebServer {
private final ExecutorService exec = Executors.newFixedThreadPool(100);
public void start() throws IOException {
ServerSocket socket = new ServerSocket(80);
//服务器没被关闭就一直接受请求
while (!exec.isShutdown()){
try {
final Socket conn = socket.accept();
exec.execute(new Runnable() {
public void run() {
handleRequest(conn);
}
});
} catch (RejectedExecutionException e) {
if (!exec.isShutdown())
//异常地拒绝了
log("task submission rejected.",e);
}
}
}
public void stop(){exec.shutdown();}
void handleRequest(Socket connection){
Request req = readRequest(connection);
//是否是代表关闭的特定HTTP请求
if (isShutdownRequest(req))
stop();
else
dispatchRequest(req);
}
}延迟任务与周期任务
- -
Timer类的缺陷
单线程带来的精确性问题
- -
线程泄漏
- -
- -
- -
- - - -
public class OutOfTime {
public static void main(String[] args) {
try {
Timer timer = new Timer();
timer.schedule(new ThrowTask(),1);
Thread.sleep(1000);
timer.schedule(new ThrowTask(),1);
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
static class ThrowTask extends TimerTask{
public void run() {
throw new RuntimeException();
}
}
}找出可利用的并行性
- -
所以并发编程最难的其实还是建模,如何从串行中挖掘出并行性。
-- -
串行的页面渲染器
- -
这个把文字的render和图片的render都归结进图像缓存的统一化思想很有意思。
-- -
- -
//串行地渲染页面元素
public class SingleThreadRenderer {
void renderPage(CharSequence source){
renderText(source);
List<ImageData> imageData = new ArrayList<>();
for (ImageInfo imageInfo : scanForImageInfo(source)){
imageData.add(imageInfo.downloadImage());
}
for (ImageData data : imageData){
renderImage(data);
}
}
}显而易见,图像的IO需要耗费大量时间,这段时间内CPU都处于空闲状态,可以说利用率非常低下。
-携带结果的Callable与Future
Callable
- -
- -
意思就是Callable比Runnable有时候更灵活,因为Callable可以抛出异常,也可以有返回值。
-Future
- -
- -
这个Future的说法很棒,只能说比起前面那个含糊的“表示一个异步执行的结果”,这个“任务的生命周期”方法更加醍醐灌顶。
-- -
public interface Future<V> {
boolean cancel(boolean mayInterruptIfRunning);
boolean isCancelled();
boolean isDone();
V get() throws InterruptedException, ExecutionException;
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}- -
public interface Callable<V> {
V call() throws Exception;其中,get的方法取决于任务的状态。
-- -
- -
可以利用返回的Future实例来对任务线程进行管理。
-- - - -
Future实现并行渲染
将要求分解为两个任务:渲染文本和渲染图像。
-- -
- - - -
//使用Future等待图像下载
public class FutureRenderer {
private final ExecutorService executor = Executors.newFixedThreadPool(80);
void renderPage(CharSequence source){
final List<ImageInfo> imageInfos = scanForImageInfo(source);
//单独开启下载图像的任务
Future<List<ImageData>> future = executor.submit(new Callable<List<ImageData>>() {
public List<ImageData> call() throws Exception {
List<ImageData> result = new ArrayList<>();
for (ImageInfo imageInfo : imageInfos)
result.add(imageInfo.downloadImage(source));
return result;
}
});
//在本线程中执行文字的渲染任务
renderText(source);
try {
//阻塞方法
List<ImageData> imageData = future.get();
for (ImageData data : imageData)
renderImage(data);
} catch (InterruptedException e) {
//重新设置线程的中断状态
Thread.currentThread().interrupt();
//不需要结果了,因而取消任务
future.cancel(true);
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
}在异构任务并行化中存在的局限
- -
- -
- -
所以难点还是分解同构任务。
-CompletionServiceExecutor和BlockingQueue
- -
CompletionService的思想其实和这个差不多。它主要就是多包装一层,数据结构的管理不用你写,更加方便。
-- -
- -
- -
也就是说ExecutorCompletionService将CompletionService的计算部分交给了传进来的线程池Executor,然后自己管理一个阻塞队列,类似生产者-消费者模式,把线程池里出来的结果放进去。
-使用CompletionService实现页面渲染器
- -
- -
public class Renderer {
//为什么这里的线程池要变成包内外界给的呢?
private final ExecutorService executor;
Renderer(ExecutorService executor){this.executor = executor;}
void renderPage(CharSequence source){
List<ImageInfo> info = scanForImageInfo(source);
//传入委托计算的线程池
CompletionService<ImageData> completionService
= new ExecutorCompletionService<>(executor);
//提交任务
for (final ImageInfo imageInfo : info)
completionService.submit(new Callable<ImageData>() {
public ImageData call() throws Exception {
return imageInfo.downloadImage();
}
});
renderText(source);
try {
for (int t = 0, n = info.size(); t < n; t++){
//得到下载结果
Future<ImageData> f = completionService.take();
ImageData imageData = f.get();
renderImage(imageData);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
}- -
疑问
我这里写了一个自己用list来保存Future结果的。不知道为什么这个不行,有待说明。
-- - - -
public class MyRenderer {
private final ExecutorService executor = Executors.newFixedThreadPool(30);
void renderPage(CharSequence source){
List<Future> res = new ArrayList<>();
List<ImageInfo> info = scanForImageInfo(source);
//提交任务
for (final ImageInfo imageInfo : info){
res.add(executor.submit(new Callable<Object>() {
public Object call() throws Exception {
return imageInfo.downloadImage();
}
}));
}
renderText(source);
try {
for (int t = 0, n = info.size(); t < n; t++){
Future<ImageData> f = res.get(t);
ImageData imageData = f.get();
renderImage(imageData);
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
}
}
}为任务设置时限
- -
- -
- -
- -
Page renderPageWithAd() throws InterruptedException{
long endNanos = System.nanoTime()+TIME_BUDGET;
//提交下载广告的任务
Future<Ad> f = exec.submit(new FetchAdTask());
//在等待广告的同时显示页面
Page page = renderPageBody();
Ad ad;
try {
//相当于timeleft=TIME_BUDGET-经过的时间,只不过进行了运算的化简
long timeLeft = endNanos- System.nanoTime();
ad=f.get(timeLeft, TimeUnit.NANOSECONDS);
} catch (ExecutionException e) {
ad = DEFAULT_AD;
} catch (TimeoutException e) {
//超时取消
ad = DEFAULT_AD;
f.cancel(true);
}
page.setAd(ad);
return page;
}- -
- -
- - - -
/**
* Attempts to cancel execution of this task. This attempt will
* fail if the task has already completed, has already been cancelled,
* or could not be cancelled for some other reason.
If successful,
* and this task has not started when {@code cancel} is called,
* this task should never run.
If the task has already started,
* then the mayInterruptIfRunning parameter determines
* whether the thread executing this task should be interrupted in
* an attempt to stop the task.
*/
/*
也就是说,如果mayInterruptIfRunning==false,就需要等该任务完成;如果==true,就直接中断
*/
boolean cancel(boolean mayInterruptIfRunning);示例:旅行预定门户网站
- -
- -
- -
也就是说,跟前面的CompletionService的优化目的是一致的,都是为了方便管理这一组future,这也跟我上面写的那个list管理版本是一样的。只不过区别在于,CompletionService还可以共用任务池,因而功能更强。invokeAll用法更简便。
-- - - -
private class QuoteTask implements Callable<TravelQuote>{
private final TravelCompany company;
private final TravelInfo travelInfo;
//...
public TravelQuote call() throws Exception{
//solicit:征求、招揽 quote:报价
return company.solicitQuote(travelInfo);
}
}
//得到排序的报价表
public List<TravelQuote> getRankedTravleQuotes(
ThravelInfo travelInfo, Set<TravelCompany> companies, Comparator<TravelQuote> ranking, long time, TimeUnit unit
)throws InterruptedException{
List<QuoteTask> tasks = new ArrayList<>();
for (TravelCompany company:companies){
tasks.add(new QuoteTask(company,travelInfo));
}
//使用invokeAll,一键定时任务,非常方便
List<Future<TravelQuote>> futures =
exec.invokeAll(tasks,time,unit);
List<TravelQuote> quotes =
new ArrayList<>(tasks.size());
Iterator<QuoteTask> taskIterator = tasks.iterator();
for (Future<TravelQuote> f : futures){
QuoteTask task = taskIterator.next();
try {
//只需调用get就行,不用传时间参数
quotes.add(f.get());
} catch (ExecutionException e) {
quotes.add(task.getFailureQuote(e.getCause()));
} catch (CancellationException e){
quotes.add(task.getTimeoutQuote(e));
}
}
//排序
Collections.sort(quotes,ranking);
return quotes;
}小结
- - - - - -
第七章 取消与关闭
- -
中断是个重要概念,也算是老朋友了
-- -
- -
任务取消
取消的原因
- -
- - - -
使用volatile标志取消
使用方法
Java并没有提供取消某个线程的安全抢占方法,仅有约定俗成的协作机制。
-比如说,可以设置一个volatile类型的取消标志,并且让线程定期查看该标志。【这是volatile的经典用途】
-- -
public class PrimGenerator implements Runnable{
private final List<BigInteger> prims
= new ArrayList<>();
//使用volatile域保护取消状态
private volatile boolean cancelled;
public void run() {
BigInteger p = BigInteger.ONE;
//任务执行时定期检查取消状态
while (!cancelled){
p=p.nextProbablePrime();
//这里用同步可能是因为下面的getPrim方法使用了同步
synchronized (this){
prims.add(p);
}
}
}
public void cancel(){cancelled = true;}
public synchronized List<BigInteger> get(){
return new ArrayList<>(prims);
}
}使用实例:
-- -
PrimGenerator generator = new PrimGenerator();
//使用Executor代替Thread
ExecutorService exec = Executors.newFixedThreadPool(1);
exec.execute(generator);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
generator.cancel();
}
System.out.println(generator.get());
exec.shutdown();- -
- - - -
缺陷
- -
比如下面程序:
-- -
public class BrokenPrimeProducer extends Thread{
private final BlockingQueue<BigInteger> queue;
private volatile boolean cancelled = false;
BrokenPrimeProducer(BlockingQueue<BigInteger> queue){
this.queue=queue;
}
public void run(){
try {
BigInteger p = BigInteger.ONE;
while (! cancelled){
//如果一直阻塞在这,便永远不会检查cancelled标志
queue.put(p=p.nextProbablePrime());
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public void cancel(){cancelled = true;}
}- -
所以解决方法其实很简单,只要让阻塞状态下我们还能知道要取消任务就行。这靠我们在表层写代码是做不到的,需要用到Java提供的另一种协作机制:线程中断。
-中断
是什么
- -
- -
所以说中断其实就是为取消而量身定做的。
-- -
- -
public class Thread{
public void interrupt();
public boolean isInterrupted();
public static boolean interrupted();
}- -
- -
所以作为上层开发者,我们仅需捕获中断异常即可。
-- -
也就是说,打断阻塞状态下的线程会清空中断状态,打断正常状态的线程会保持中断状态。而正常状态的线程如果不对中断状态处理,就会一直保持中断状态然后继续运行,也就是屏蔽中断状态。
-- -
具体检查方法还是定期看标记。在看到标记后可以做善后工作再决定停不停。
-- -
程序清单5-10:
-- -
恢复中断状态的示例:
-- -
捕获睡眠时的中断异常,然后重新设置打断标志为true,进入下一次循环时再对标记进行处理。
-- - - -
程序示例
- -
//注意此处继承自Thread
public class PrimeProducer extends Thread{
private final BlockingQueue<BigInteger> queue;
PrimeProducer(BlockingQueue<BigInteger> queue){
this.queue = queue;
}
public void run() {
try {
BigInteger p = BigInteger.ONE;
//put本就可以检测中断,为啥还要外层包装一层检测的while呢?书中说是为了提高相应度。
while(!Thread.currentThread().isInterrupted()){
queue.put(p = p.nextProbablePrime());
}
} catch (InterruptedException e) {
//此时catch完之后自动退出
/* 允许线程退出 */
}
}
public void cancel(){interrupt();}
//这个是我为了方便调试自己加的方法
public synchronized void get(){
for(BigInteger i : queue){
System.out.println(i.toString());
}
}
}测试主函数部分如下:
-- - - -
PrimeProducer generator = new PrimeProducer(new ArrayBlockingQueue<>(10));
generator.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
generator.cancel();
}
generator.get();一个有待解决的疑问
此处编写主函数运行时,不小心产生了一个错误:Why does the ThreadpoolExecutor code never stop running?
-- -
public static void main(String[] args) {
PrimeProducer generator = new PrimeProducer(new ArrayBlockingQueue<>(10));
ExecutorService exec = Executors.newFixedThreadPool(1);
exec.execute(generator);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
generator.cancel();
}
//generator.get();
exec.shutdown();
}这段代码跑起来的最终结果就是进程永远无法终止。至于为什么:
-PrimeProducer类继承自Thread,而execute的参数是一个Runnable。也就是说,Executor会把传进来的这个Thread当成一个Runnable,然后再把它包装成一个新的Thread。所以你的generator里的cancel方法:
-- -
public void cancel(){interrupt();}调用的就不是本线程的中断方法,而是一个全新的毫无关系的线程的中断方法了。
-所以其实应该这么写:
-- -
public static void main(String[] args) {
PrimeProducer generator = new PrimeProducer(new ArrayBlockingQueue<>(10));
generator.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
generator.cancel();
}
generator.get();
}但我还是有个奇思妙想。可不可以沿用一开始那个错误的主方法版本,然后修改PrimeProducer类为:
-- -
//此处修改为Runnable
public class PrimeProducer implements Runnable {
//......
//此处修改
public void cancel(){Thread.currentThread().interrupt();}
//......
}结果还是跑不起来,不知道为什么,有待解答。
-中断策略
- -
- -
- -
意思就是,单个的任务是非线程所有者,因为它们是被分配到线程池所有的线程执行的。所以它们不能直接对中断进行处理,需要把中断异常抛给那个目前还不知道是谁的所有者线程,让调用者决定自己该怎么做。
-- -
- -
- -
- - - - - -
以下的地方一个字也看不懂,写自己的思考也没什么意义。就附上正确代码模板吧。
-- -
//通过future定时取消任务
private static final ScheduledExecutorService taskExec =
Executors.newScheduledThreadPool(10);
public static void timeRun(Runnable r,
long timeout, TimeUnit unit)
throws InterruptedException {
Future<?> task = taskExec.submit(r);
try {
task.get(timeout,unit);
} catch (ExecutionException e) {
throw launderThrowable(e.getCause());
} catch (TimeoutException e) {
//接下来任务将被取消
} finally {
task.cancel(true);
}
}- - - -]]>
可重定位的代码通过linker和loader重定位这部分内容就是在之前那本书学过的。
-从中,我们也可以看到有语法分析、中间代码的影子。
-词法分析相当于通过DFA NFA捉出各类符号,形成简单的符号表和token list;语法分析相当于对token list组词成句,判断该句子是否符合语言规则;语义分析相当于对词句进行类型判断和中间代码的生成,获得基本语义。
-语法制导翻译:语义分析和中间代码生成集成到语法分析中
-将结果转化为token的形式。
-从token list中识别出各个短语,并且构造语法分析树。
-相当于是通过文法来进行归约(自底向上的语法分析),从而判断给定句子是否合法。
-种属就是比如是函数还是数组之类的。
-静态绑定
-包括绑定代码相对地址(子程序)、数据相对地址(变量)
-波兰也就是前序遍历二叉树(中左右),逆波兰也就是后序遍历二叉树(左右中)
-无关机器
-有关机器
-这也挺好理解,相当于管理符号表吧。
-了解了编译程序的基本结构,那么我们就可以想想该怎么实现这个编译器了。
-最直观的想法是,我们有几个步骤就对代码进行多少次扫描:
-也就是说:
-帅的。
-这大概是描述了我们到时候会怎么实现这两个阶段代码。
-不过确实,词法分析可以看作是正则匹配,语法分析可以看作是产生式。
-字母表
-串
-克林闭包中的每一个元素都称为是字母表Σ上的一个串
-如果文法用于描述单词,基本符号就是字母;用于描述句子,基本符号就是单词
-文法的形式化定义
-由于可以从它们推出其他语法成分,故而称之为非终结符
-还真是最大的语法成分
-产生式
-符号约定
-文法符号串应该就是指既包含终结符也包含非终结符的,也可能是空串的串。
-注意终结符号串也包括空串。
-这部分就是要讲怎么看一个串是否满足文法规则,那么我们就需要先从什么样的串是满足文法规则的串开始说起,也即引入“语言”的概念。
-推导与归约
-然后也分为最左推导和最右推导,对应最右归约和最左归约。
-故而,如果从开始符号可以推导(派生)出该句子,或者从该句子可以归约到开始符号,那么该句子就是该语言的句子。
-句子与句型
-句型就是可以有非终结符,句子就是只能有终结符
-语言
-文法解决了无穷语言的有穷表示问题。
-emm,就是好像没有∩运算
-有正则那味了
-0型
-1型
-之所以是上下文有关,是因为只有A的上下文为a1和a2时才能替换为β【666666,第一次懂】
-CSG不包含空产生式。
-2型
-左部只能是一个非终结符。
-3型
-产生式右部最多只有一个非终结符,且要在同一侧
-看起来还能转(是的,自动机教的已经全忘了())
-正则文法用于判定大多数标识,但是无法判断句子构造。
-也就是说,每个句型都有自己对应的分析树。那么接下来就介绍什么是句型的短语
-意思就是直接短语是高度为2的子树的边缘,直接短语一定是某个产生式的右部,但是产生式右部不一定是给定句型的直接短语(因为有可能给定句型的推导用不到那个产生式)
-通过自定义规则消除歧义
-最后两条值得注意
-所以真正的终止是输入带到末尾并且指向终态
-66666,还能这么捏起来
-关键字是在识别完标识符之后进行查表识别的
-说实话没太看懂
-根据给定文法,识别各类短语,构造分析树。所以关键就是怎么构建分析树
-可以看做是推导(派生)的过程。
如果同一非终结符的各个产生式的可选集互不相交,就可以进行确定的自顶向下分析:
这两个分析也是我们的分析方法需要解决的。
-也就是说,在自顶向下分析时,采用的是最左推导;在自底向上分析时,最左归约和最右推导才是正道!
-大概流程应该是,有产生式就展开,然后当产生式右部有多个候选式的时候再根据输入决定。
-如果有多个以输入终结符打头的右部候选,那就需要逐一尝试错了再回溯,因而效率较低。
-66666,这其实就可以类似于动态规划了吧
-【感觉这里也能窥见一些算法设计的思想。
-仔细想想,我们在引入动态规划时,也是这个说辞:对于一些回溯问题,回溯效率太低,所以我们就可以提前通过动态规划的思想构造一个状态转移表,到时候只需从零开始按照表进行状态转移即可。
-仔细想想,这不就是这里这个预测分析提出的思想吗!真的牛逼,6666
-我记得KMP算法一开始也是这个思想,感觉十分神奇】
-这个左递归及其消除方法解释得很形象
-先转化为直接左递归
-666666这个解读可以,感觉这个就跟:
-这个“向前看”有异曲同工之妙了。
-LL(1)文法才能使用预测分析技术。判断是否是LL文法就得看具有相同左部的产生式的select集是否相交。
-S文法不包含空产生式
-也就是说,B的Follow集为{b,c},只有当输入符号为b/c时才能使用空产生式。
-first集和follow集不交。
-这下总算知道这两个是什么玩意了。也就是这样:
-输入符号与B的First集元素匹配
-直接用那个产生式
-否则,看输入符号是否与Follow集元素匹配
-是
-若B无空产生式,报错;否则,使用B的空产生式(相当于消了一个符号但不变输入带指针)
-否
-报错
-这个感觉跟first集有点像,相当于是右部只能以终结符开始的形式,所以下面的LL文法会增强定义。
-当该非终结符对应的所有SELECT集不相交,就可以进行确定的自顶向下语法分析。这个思想也将贯穿下面的LL文法
-最后,如果同一非终结符的各个产生式的可选集互不相交,就可以进行确定的自顶向下分析:
-这几个推理下来,真是让人感觉酣畅淋漓!
-确定的自顶向下分析的核心就是,给定一个当前所处的非终结符和一个输入字符[E, a],我们可以唯一确定一个产生式P用于构建语法分析树。
-也即,同一个非终结符的所有产生式的SELECT集必须是不交的【才能确保选择产生式的唯一性】。因而,问题就转化为了如何让SELECT集不交。
-我们需要对空产生式和正常产生式的SELECT集计算做一个分类讨论。
-空产生式
-由于可以推导出空,相当于把该符号啥了去读下一个符号,因此我们的问题就转化为输入字符a是否能够跟该符号后面紧跟着的字符相匹配。而紧跟着的字符集我们将其成为FOLLOW集,如果a在follow集中,那么就可以接受,否则不行。
-对于LL(1)文法,相当于是进一步处理了简介推出空的串:
- 由于α串->*空,则α串必定仅由非终结符构成。那么它能推导出的所有可能即为SELECT集。故而为First(α)∪Follow(α)
非空产生式
-很简单,就是其First集。
-故而,只需要让这些计算出来的First集合不交,就能进行确定的自顶向下语法分析,构造确定的语法分析树。不得不说真的牛逼。
-感觉其“预测分析”的“预测”主要体现在对空产生式的处理上。
-总算懂了为什么LL(1)能够解决这个回溯效率太低的问题了,太牛逼。不过问题是怎么转化为LL(1)呢()上面的消除回溯和左递归只是一部分而已吧。
-这个消除二义性是啥玩意?二轮的时候看看PPT怎么讲的
-66666,它这个计算follow集的方法就很直观
-declistn有个空产生式,那么我们看得看②,而②的declistn排在最后,也就是说declistn的follow集就是其左部declist的follow集【6666】,所以我们看①,可以发现declist后面为:。
-如果是终结符,就直接==比较;非终结符,就把token传入到其对应的过程。
-66666
-感觉从中又能窥见动态规划的同样思想了。下推自动机其实感觉就像是递归思想(或者说顺序模拟递归,因为它甚至有一个栈,出栈相当于达成条件递归return),动态规划的话可能有点像是把每个不同状态以及不同状态时的栈顶元素整成一个2x2的表,所以感觉思想类似。
-注意,是栈顶跟输入一样都是非终结符才会移动指针和出栈
-值得注意的是,输出的产生式序列就对应了一个最左推导。
-其实也挺有道理,栈顶是非终结符,但是输入是它的follow集,那我们自然而然可以想到把这b赶跑,看看下面有没有真的它的follow集在嗷嗷待哺。
-正确识别句柄是一个关键问题。
-句柄:当前句型的最左直接短语。【最左、子树高度为2】
-每次句柄形成就将它归约,因而保证一直是最左归约(recall that,句柄一定是某个产生式的右部,并且每次最左句柄一旦形成就归约)
-正如上面的LL分析,每次推导要选择哪个产生式是一个问题;这里的LR分析,每次归约要选择哪个产生式,也即正确识别句柄,也是一个关键问题。
-所以,我们应该把句柄定义为当前句型的最左直接短语。
-如下图所示,左下角是当前句型(画红线部分)的语法分析树,红字为在栈中的部分,蓝字为输入符号串剩余部分。当前句型的直接短语(相当于根节点的高度为二的子树,或者说子树前两层)有两个,一个是以<IDS>
为根节点的<IDS> , iB
,另一个是<T>
为根节点的real
。
而LR分析技术的核心就是正确地识别了句柄。
-也就是说LR技术就是用来识别句柄的,识别完了句柄就可以构建类似自顶向下的预测分析那样的自动机表来进行转移。
-移进状态
-·后为终结符
-待约状态
-·后为非终结符
-归约状态
-·后为空
-以前感觉一直很难理解GOTO表的作用,现在感觉稍微明白了点了,你想想,归约之后的那个结果是不是有可能是另一个产生式的右部成分之一,也即一个新的句柄?并且这个也是由你栈顶刚归约好的那个左部和下面的输入符号决定的。那么你自然而然需要切换一下当前状态,以便之后遇到那个产生式的时候能发现到了。
-那么,剩下的问题就是如何构造LR分析表了:
-也就是它会整一个终结符之间的优先级关系。。。
-也就是说:
-a=b
-相邻
-a<b
-也即在A->aB时,b在FIRSTOP(B)中(理解一下,这个First指在前面。。。)
-a>b
-也即在A->Bb时,a在LASTOP(B)中(理解一下,这个LAST指在后面。。。)
-我服了
-好像#这个固定都是,横的为左,竖的为右
-根据优先关系来判断移入和归约
-每个分析方法其实都对应着一种构造LR分析表的方法。
LR(0)通过构造规范LR0项集族,从而构造LR分析表,从而构造LR0 DFA来最终进行语法分析。
每一个项目都对应着句柄识别的一个状态。
-而肯定不可能整那么多个状态,所以我们需要进行状态合并。(这样也就很容易理解LR的状态族构建了。)
-它这里也很直观解释了为什么点遇到非终结符就需要加入其对应的所有产生式,因为在等待该非终结符就相当于在等待它的对应产生式的第一个字母。
-上面这东西就是这个所谓的规范LR(0)项集族了。
-但是会产生移进归约冲突:
-还有归约归约冲突:
-所以我们就把没有冲突的叫LR(0)文法。
-感觉上述两个问题都是因为有公共前缀【包括空产生式勉强也能算是这个情况】,导致信息不足无法判断应该怎么做,多读入一个字符(也即LR(1))应该可以有效解决该问题。
-其实本质还是识别句柄问题,也即此时是归约还是移入,得看是不是句柄。故而LR0信息已经不能帮我们识别句柄了。
-Follow集可以帮助我们判断。由该状态I2可知,输入一个*应该跳转到I7。如果在I2把T归约为一个E,由Follow集可知E后面不可能有一个*,也就说明在这里进行归约是错误的,应该进行移入。
-这种依靠Follow集和下一个符号判断的思想,就会运用在SLR分析中。
-但值得注意的是SLR分析的条件还是相对更严苛,它要求移进项目和归约项目的Follow集不相交,所以它也会产生像下图这样的冲突:
-SLR将子集扩大到了全集,显然进行了概念扩大。
-含义为只有当下一个输入符号是XX时,才能运用这个产生式归约。这个XX是产生式左部非终结符的Follow子集。
-这玩意只有归约时会用到,这个很显然,毕竟前面提到的LR0的问题就是归约冲突。
-对了,值得注意的是这个FIRST(βa)
,它表示的并不是FIRST(a)∪FIRST(β)
,里面的βa应该取连接意,也即,当β为非空时这玩意等于FIRST(β)
,当β空时这玩意等于FIRST(a)
。
刚刚老师对着这个状态转移图进行了一番强大的看图写话操作,我感觉还是十分地牛逼。她从这个图触发,讲述了状态I2为什么不能对R->L进行归约。
-假如我们进行了归约,那么我们就需要弹出状态I2回到I0,压入符号R,I0遇到符号R进入了I3,I3继续归约回到I0,I0遇到符号S到状态I1,但1是接收状态,下一个符号是=不是$,所以错了。
-比如说I8和I10就是同心的。左边的那个实际上是LR0项目集,所以这里的心指的是LR0。
-然而,LR(1)会导致状态急剧膨胀,影响效率,所以又提出了个LALR分析。
-跟前面的SLR对比可以发现,相当于它就是多了个逗号后面的条件。但是这是可以瞎合的吗?不会出啥问题不。。。
-好吧问题这就来了,LALR可能会产生归约归约冲突。但值得注意的是,它不可能出现归约移入冲突,因为LR1没有这个东西,而LALR只是修改右边的符号,所以也不会有这个。
-因为LALR实际上是合并了展望符集合,这东西与移进没有关系,所以只会影响归约,不会影响移进。
-LALR可能会产生归约归约冲突。但值得注意的是,它不可能出现归约移入冲突,因为LR1没有这个东西,而LALR只是修改右边的符号,所以也不会有这个。
-它有可能做多余的归约动作,从而推迟错误的发现。
-形式上与LR1相同;大小上与LR0/SLR相当;分析能力介于SLR和LR1之间;展望集仍为Follow集的子集。
-感觉一路看下来,思路还是很流畅的。LR0会产生归约移进冲突和归约归约冲突,所以我们在归约时根据下一个符号是在移进符号还是在Follow集中来判断是要归约还是要移进。但是SLR条件严苛,对于那些移进符号集和Follow集有交的不适用,并且这种情况其实很普遍。加之,出于这个motivation:其实不应该用整个Follow集判断,而是应该用其真子集,所以我们开发出来个LR1文法。然后LR1文法虽然效果好但是状态太多了,所以我们再次折中一下,造出来个效果没有那么好但是状态少的LALR文法。
-所以我们可以用LR对二义性文法进行分析
-我们可以通过自定义规则来消除二义性文法的归约移入冲突
-对于状态7,此时输入+ or *会面临归约移入冲突。由于有E->E+E归约式子,可以知道此时栈中为E+E。当输入*,由于*运算优先级更高,所以我们在此时进行移入动作转移到I5;当输入+,由于同运算先执行左结合,所以我们此时可以安全归约。
-对于状态8,由于*运算比+优先级高,且左结合,所以始终进行归约。
-它这个意思大概就是,符号栈和状态栈都一直pop,直到pop到一个状态,GOTO[符号栈顶,状态栈顶]有值【注意,始终保持符号栈元素+1 == 状态栈元素数+1
】。然后,一直不断丢弃输入符号,直到输入符号在A的Follow集中。此时,就将GOTO值压入栈中继续分析。
【这其实也很有道理。如果输入符号在A的Follow集,说明A之后很有可能可以消耗这个输入符号。】
-注意:
-思想:
-也可能是先入为主吧,感觉用实验的方法来理解语义分析比较便利。语义分析相当于定义一连串事件,附加在每个产生式上。当该产生式进行归约的时候,就执行对应的语义事件。而由于执行语义分析时需要的符号在语法分析栈中,所以我们也同样需要维护一个语义分析栈,在移进时也需要进栈。
-语义分析一般与语法分析一同实现,这一技术成为语法制导翻译。
-可以回忆一下实验,相当于对每个产生式进行一个switch-case,然后依照产生式的类别和代码规则进行出栈入栈来计算属性值。
-一个很简单区分综合属性和继承属性的方法,就是如果定义的是产生式左部的属性,那就是综合属性;右部,那就是继承属性
-这个东西就是我们实验里写的,副作用也是更新符号表。
-没有副作用的SDD称为属性文法。
-而感觉语法分析这个过程的产生式归约顺序就能一定程度上表示了这个求值顺序
-蛤?这不是你自己规则设计有问题吗,关我屁事
-其实我还是不大理解,因为这个规则不是user定义的吗?所以产生环不也是它的事,难道说自顶向下或者自底向上分析还能优化SDD定义??
-感觉它意思应该是这样的,有一个方法能绝对不产生循环依赖环,也即将自底向上/自顶向下语法分析与语义分析结合的这个方法。这个方法就是它说的真子集。
-所以我们接下来要研究的就是什么样的语义分析可以用自顶向下or自底向上语法分析一起制导。
-那确实,你自底向上想要计算继承属性好像也不大可能
-对应了自顶向下的最左推导顺序
-S-SDD包含于L-SDD
-当归约发生时执行对应的语义动作
-还需要加个属性栈
-所以S-SDD+自底向上其实很简单,因为只需在归约的时候进行语义分析,在移进的时候push进属性栈就行了。
-具体的S-SDD结合语法分析的分析过程可以看视频。
-这个例子还算简单的,毕竟只是综合属性的计算而已,只需要加个属性栈,保存值就行了。
-我们可以来关注一下这个SDT的设计,也很简单。可以产生式和语义规则分离看待,这也给我们以后设计提供一定的启发。
-这个是自顶向下的语法分析,本来只用一个栈就行了,现在需要进行扩展。T的综合属性存放在它的右边,继承属性存放在它的平行位置。
-当属性值还没计算完时,不能出栈;当综合记录出栈时,它要将属性值借由语义动作复制给特定属性。
-然后语义动作也得一起进栈。
-digit是终结符,只有词法分析器提供值
-此时,digit跟一个语义动作关联,所以我们需要把它的值复制给它关联的这个语义动作{a6},然后才能出栈。
---关联的另一个实例:
--
此时由于T’.inh还要被a3用到,所以我们就得在T’出栈前把它的这个inh值复制给a3。
-
当遇到语义动作之后,就执行动作,并且出栈语义动作。
-它这意思应该是遇到每个产生式的每个符号要执行什么动作都是确定的,所以代码实现是可能的。
-可以看到:
-666666666
-感觉这个值得深思,但反正现在的我思不出啥了。。。
-相当于把L-SDD转化为了个S-SDD。具体是这样,把原式子右边的变量替换为marker的继承属性,结果替换为marker的综合属性。那么新符号继承属性怎么算啊。。。不用担心,因为观察可知要使用的这两个非终结符一定已经在栈中了。
-具体分析也看视频就好了。
-false list就是if失败后的那个goto序号,true list是成功的那个goto序号,s.nextline是整个if的下一条指令
-增量生成
-它这个相当于是把符号表和offset都整成了一个栈,毕竟确实过程调用就是得用栈结构的
-之后用到该记录类型,就指向记录符号表即可。
-这个就不用填符号表了,所以helper function都是用来产生中间代码的
-addr属性需要从符号表中获取
-看个乐吧
-在语义动作中实现
-反正意思就是用S.next这个继承属性来表示S.code执行完后的下一个三地址码地址。
-其实不大懂这什么玩意
-抽象
-这两个都是综合属性
-相当于是一个waiting list
-可以理解为,B这个表达式可以分为两种情况,两种情况有一个为真B就为真。那么,B的真回填list相当于也被分为了两种情况,所以要求B的就是把它们合起来。
-原来回填是这个意思
-nextline是一个综合属性
-TODO 这笔之后再看。。。。
-TODO
-静态链也被称作访问链,用于访问存放于其他活动记录中的非局部数据。
-动态链也被称作控制链,用于指向调用者的活动记录。
-反正意思就是既要得到原来的A,又要修改A
-也就是说左边及其所有子树全调完了,才能调下一个兄弟的。
-左边这几点设计规则都十分reasonable,很值得注意。
-不过我其实挺好奇,参数存在那么后面该咋访问。。。。看xv6,似乎是fp指向前面,sp才指向local,也即用了两个栈指针。
-这个控制链也是约定俗成的,具体可以想起来xv6也是类似结构:
-当函数返回的时候,就会进行恢复现场,从而出栈一直到ra,很合理。
-调用序列应该就是设置参数、填写栈帧一类,返回序列就是恢复现场
-传变量、改变meta data、改变top和sp指针
-这段解释了下为什么不用堆,说得很好
-第二点,比如malloc后不free
-每一个嵌套深度的分配一个Display位
-S嵌套深度1,所以占据d[1];Y和X嵌套深度2,所以占据d[2];Z嵌套深度3,所以占据d[3]。
-然后,一开始遇到个S,d1指向S;然后调用Y,d2指向Y;然后Y中调用X,就修改d2指向X;然后调用Z,就修改d3指向Z。
-总之显示栈就是这个变换指针的过程。
-至于控制栈,要打印这里面的display表,就是看层数。如果d1那就打印当前层,d2就打印的12层,d3就123层【不是纯显示栈,是它自己内部的未变换指针的结果】
-结果:SXZ
-静态作用域是空间上就近原则,动态是时间上。
-也就是说这时候非局部的一定是全局变量或者静态的局部变量。
-如果是支持过程声明嵌套,顺着符号表就可以找到其父过程/子过程的数据。
-符号表也可以用于构造访问链,因为过程名也是一种符号。
-不讨论这个
-所以这东西是用来决策寄存器分配的
-反正类似保护现场恢复现场
-在思考自动机和动态规划的关系时,胡乱搜索看到了AC自动机,于是来了解了一下。
-- --
--考虑一个问题:给出若干个模式串,如何构建一个DFA,接受所有以任一模式串结尾(称为与该模式串匹配)的文本串?
-可以先思考一个更简单的问题:如何构建接受所有模式串的DFA?很明显,**字典树**就可以看做符合要求的自动机。例如,有模式串
-"abab"
、"abc"
、"bca"
、"cc"
,我们把它们插入字典树,可以得到:-
为了使它不仅接受模式串,还接受以模式串结尾的文本串,一个看起来挺正确的改动是,使每个状态接受所有原先不能接受的字符,转移到初始状态(即根节点)。
--
但是如果我们尝试
-"abca"
,我们会发现我们的自动机并不能接受它。稍加观察发现,我们在状态5接受a
应该跳到状态8才对,而不是初始状态。某种意义上来说,状态7是状态5退而求其次的选择,因为状态7在trie上对应的字符串"bc"
是状态5对应的字符串"abc"
的后缀。既然状态5原本不能接受"a"
,我们完全可以退而求其次看看状态7是否可以接受。这看起来很像KMP算法,确实,AC自动机常常被人称作trie上KMP。所以我们给每个状态分配一条fail边,它连向的是该状态对应字符串在trie上存在的最长真后缀所对应的状态。我们令所有状态p接受原来不能接受的字符c,转移到 next(fail(p),c) ,特别地,根节点转移到自己。为什么不需要像KMP算法一样,用一个循环不断进行退而求其次的选择呢?因为如果我们用BFS的方式进行上面的重构,我们可以保证 fail(p) 在p重构前已经重构完成了,类似于动态规划。
--
这样建fail边和重构完成后得到的自动机称为AC自动机(Aho-Corasick Automation)。
-我们发现fail边也形成一棵树,所以其实AC自动机包含两棵树:trie树和fail树。一个重要的性质是,如果当前状态 p 在某个终止状态 s 的fail树的子树上,那么当前文本串就与 s 所对应模式串匹配。
-
也就是说它的解决方法是加fall边(蓝色)和加新边(红色),
-]]>JUnit是白盒测试。
-包含各种测试用例。
-一般放在包名xxx.xxx.xx.test里,类名为“被测试类名Test”。
-测试方法可以独立运行。
-方法名一般为“test测试的方法”,void,空参。
-Assert.assertEquals(3,result); |
@Before在所有测试方法执行前自动执行,常用于资源申请。
-@After在所有测试方法执行完后自动执行,常用于释放资源。
-反射是框架设计的灵魂。
-类加载器把硬盘中的字节流文件装载进内存,并且翻译封装为Class类对象。通过Class类对象才能创建Person对象。
-而这也就是说,如果我们有了Class对象,我们就可以创建该类对象。
-有三种方式。
-将字节码文件加载进内存,返回class对象。多用于配置文件【将类名定义在配置文件】
-注意:类的全名指的是包.类,包含包名。
-通过类名的属性class获取。多用于参数传递。
-getClass()是Object类的方法。多用于对象的获取字节码的方式。
-//第一种方式 |
同一个字节码文件(*.class)在一次程序运行过程中只会被加载一次,不管是以哪种方式得到的Class对象,都是同一个。
-可以通过class对象得到其字段、构造方法、方法等。
-class Student{ |
Student stu = new Student("张三",321,1000,57.7); |
常用方法:
-//获取所有公有字段 |
Field f = stuC.getDeclaredField("money"); |
获取方法跟上面格式差不多。
-//获取构造方法 |
如果想要获取公有的无参构造器,还可以使用Class类提供的更简单的方法,不用先创造构造器:
-System.out.println(stuC.newInstance()); |
//获取方法 |
public class ReflectTest { |
①和③都是jdk预定义的。自定义主要是②。
-javadoc XXX.java |
会自动根据里面的注解生成文档
-
|
本质上
-public Override{} |
等价于
-public interface Override extends java.lang.annotation.Annotation{} |
注解的属性就是接口中的成员方法。要求无参,且返回类型有固定取值:
-public MyAnno { |
|
描述注解的注解
-RetentionPolicy的三个取值:SOURCE、CLASS、RUNTIME,正对应着java对象的三个阶段。
-SOURCE:不保留到字节码文件,会被编译器扔掉
-CLASS:保留到字节码文件
-RUNTIME:被读到
-自定义的注解一般都取RUNTIME。
-相当于用注解替换配置文件
-
|
|
class.getAnnotation(Pro.class);
这句话实质上是创建了一个实例,继承了Pro接口,重载了里面的抽象方法。
|
然后在要测试的每个方法上面加上此标签。
-然后编写test方法:
-public class TestCheck { |
mysql -h[IP地址] -u[用户名] -p |
本地的一个文件夹就代表一个数据库,文件夹里的一个文件代表一张表。
-SQL有四种语句类型
-create datebase 数据库名称; |
drop database 数据库名称; |
alter database 数据库名称 charactor set 修改后新值; |
show databases;# 查询所有数据库名称 |
select database();# 查询正在使用的数据库名称 |
create table students( |
*注:
-mysql的数据类型表
-其中:
-① double(3,1)表示XXX.X,最大值为99.9.
-② 关于三个时间类型
-所以timestamp常用作插入时间。
-③ varchar(20)表示二十个字符长的字符串。
-注意,是“二十个字符”而不是“二十个字节”。如果使用的字符集每个字符占3个字节,则varchar(20)占60个字节。
-④ BLOB、CLOB、二进制这些用于存储大数,不常用
-drop table 表名; |
# 修改表名 |
show tables;# 查询数据库中所有表的名字 |
insert into students(name,age,score,birthday) values('张三',15,99.9,"2022-12-5"); |
如果不加条件,会把表中所有数据删除
-delete from students where name="张三"; |
如果不加条件,会把表中所有记录全部修改
-update students set name="1", age=10 where name="张三"; |
select # 多字段查询 |
基本运算符
-<、>、=、<=、>=、<>(不等于,也可以用!=)
-逻辑运算符
-AND、OR
-BETWEEN AND
-IN后跟集合
-IS、IS NOT
-LIKE 模糊查询
-类似正则使用占位符匹配
-select * from students where name like "马%"; |
order by 排序字段1 排序方式1,排序字段2 排序方式2; |
默认升序。
-ASC、DESC
-多关键字排序
-第二条件仅当第一条件一样才使用。
-将一列数据作为整体,纵向计算
-注意,聚合函数的计算会排除NULL值。如果不想让空置排除,可以尝试该方法:
-select count(ifnull(math,0)) from students; |
count 计算个数
-select count(name) from students;# 有多少条记录 |
一般如果要看有多少记录,可以用count(主键),因为主键不为空。
-max、min
-sum 求和
-avg 平均值
-分组之后查询的字段只能是两种:① 分组字段 ② 聚合函数。因为分组了之后再查具有个人特色的东西就没意义了。【高版本的mysql如果查询别的字段会报错】
-select sex,avg(math) from students group by sex; |
还可以在分组前对条件限定,使用WHERE
-select sex,avg(math) from students where math >= 70 group by sex; |
或者在分组后限定,使用HAVING
-select sex,avg(math),count(id) from students group by sex having count(id)>2; |
这种就是分页查询。
-limit 开始的索引,每页查询的条数; |
limit
只能在mysql使用。
用户表存放地点↑
-USE mysql; |
注意,以下出现的”用户名”@”主机名” IDENTIFIED BY “密码”,不能在@两侧加空格,否则报错。
-CREATE USER "用户名"@"主机名" IDENTIFIED BY "密码"; |
DROP USER "用户名"@"主机名"; |
-- 使用mysql自带的密码加密函数PASSWORD |
SHOW GRANTS FOR "root"@"%"; |
grant 权限列表 on 数据库名.表名 to '用户名'@'主机名'; |
revoke 权限列表 on 数据库名.表名 from '用户名'@'主机名'; |
CREATE TABLE stu( |
如果要去掉该约束,可以这么做:
-ALTER TABLE stu MODIFY name VARCHAR(20); |
由于我们没写“NOT NULL ”,所以非空约束就被去掉了。感觉这点的解释挺有意思的。
-某列值不能重复
-CREATE TABLE stu( |
但是注意,唯一约束允许多个NULL存在。
-唯一约束的删除方法跟前面的非空约束就完全不一样了。
-ALTER TABLE stu DROP INDEX phone_number; |
--创建唯一约束时会自动创建唯一索引,需要删除索引
-
一张表只能有一个主键。主键非空且唯一。
-CREATE TABLE stu( |
ALTER TABLE stu DROP PRIMARY KEY; |
这东西一般都跟主键结合使用。
-若某一列是数值类型,可以使用auto_increment关键字来完成值的自动增长。
-CREATE TABLE stu( |
自动增长的数据只跟上一个记录有关系。
-表中dep_name和dep_location有数据冗余,修改或者插入都不方便,不符合数据库设计准则,所以需要创造两张表。
-但要是你想裁员了,直接在第二个表删研发部是没用的,第一个表数据还在,还得麻烦地一个个删。这时候外键就起作用了。
-外键只能关联唯一约束或者主键约束的列。一般外键都是去关联主表的主键。
-CREATE TABLE employee( |
此时不能删除department的行【要是该行在employee表没出现过的话就可以删掉】,也不能在employee添加一个不存在的外键值。
-ALTER TABLE employee DROP FOREIGN KEY emp_dept_fk; |
如果你想修改外表主键值,就需要用到级联更新。
-ALTER TABLE employee ADD CONSTRAINT emp_dept_fk FOREIGN KEY (dep_id) REFERENCES department(id) ON UPDATE CASCADE ;-- 外键声明+级联更新声明 |
如果你想达到删除一个主键值就能删除表中的所有与该主键值关联的数据,就需要用到级联删除。
-ALTER TABLE employee ADD CONSTRAINT emp_dept_fk FOREIGN KEY (dep_id) REFERENCES department(id) ON DELETE CASCADE ;-- 外键声明+级联删除声明 |
级联使用应该要谨慎。一是它不大安全,二是它涉及多表操作,效率低下
-1NF中的主属性为学号和课程名称。可以看到,分数完全依赖于码,但是姓名、系名、系主任都只是部分依赖于码,这不符合2NF的条件。因而,我们就可以选择拆分表,把完全依赖的部分和部分依赖的部分分开:
-由于分数->(学号,课程名称),因而可以把学号、课程名称、分数放在一张表
-由于姓名、系名、系主任 ->(学号),因而可以把学号、姓名、系名、系主任放在一张表
-如下图所示。这样就消除了部分依赖。
-2NF中选课表的主属性为学号和课程名称,学生表的主属性为学号。可以看到,学生表中,存在着系主任->系名->学号这样的传递依赖,不符合3NF的规定。因而,我们需要对学生表进行进一步的拆分。
-我们为了破坏系主任->系名->学号这个传递链,可以拆分成系主任->系名和系名->学号两个传递关系。
-因而,可以把学生表拆分为如下图两张表:
-使用where条件
--- 查询所有员工信息和对应的部门信息 |
--此在书中称为“等值连接和非等值连接”。
-
语法: select 字段列表 from 表名1 [inner] join 表名2 on 条件
-SELECT * FROM emp INNER JOIN dept ON emp.`dept_id` = dept.`id`; |
--关于外连接和内连接的区别,以及左外连接与右外连接的区别:
--
![屏幕截图 2022-12-19 155750](./JavaWeb/屏幕截图 2022-12-19 155750.png)
--
语法:select 字段列表 from 表1 left [outer] join 表2 on 条件;
-SELECT t1.*,t2.`name` FROM emp t1 LEFT JOIN dept t2 ON t1.`dept_id` = t2.`id`; |
语法:select 字段列表 from 表1 right [outer] join 表2 on 条件;
-SELECT * FROM dept t2 RIGHT JOIN emp t1 ON t1.`dept_id` = t2.`id`; |
查询嵌套
---子查询中不允许使用ORDER BY
-在实际运用中,内连接比子查询的效率更高
-
---
可用于WHERE条件
--- 查询工资最高的员工信息 |
可以作为条件用IN关键字
--- 查询'财务部'和'市场部'所有的员工信息 |
可以当做一个新表,可以转化为普通内连接
--- 查询员工入职日期是2011-11-11日之后的员工信息和部门信息 |
---
子查询内部使用了父查询的东西
--
-
一个包含多个步骤的业务操作被事务管理,操作要么同时成功,要么同时失败。【有种原子操作的感觉?】
-当操作失败时,会回滚到执行前的状态。
-事实上就是类似有个缓冲区,得到commit指令就把缓冲区内容更新,得到rollback指令就把缓冲区内容丢弃。
--- 开启事务 |
-- 开启事务 |
START TRANSACTION; |
ROLLBACK; |
COMMIT; |
一条DML(增删改表中数据)语句默认会自动提交。但如果手动开启了事务,那么事务内保护的原子序列就需要手动提交。
-如果想将默认提交给kill了,也即不论是否开启事务都得手动提交,那么就需要用如下语句:
-SET @@autocommit = 0; |
一旦事务提交/回滚,会持久性更新数据库表。
-多个事务之间应该相互独立。为了保障这一点,需要设置事务的隔离级别。
-事务操作前后数据总量不变。
-概念:多个事务之间隔离的,相互独立的。但是如果多个事务操作同一批数据,则会引发一些问题,设置不同的隔离级别就可以解决这些问题。【有点并发的感觉】
-隔离级别越高,安全性越高,效率越来越差。
-mysql默认的是3,oracle默认的是2.
-可以设置隔离级别。
-set global transaction isolation level "级别字符串"; |
--通过之后老师说的内容,感觉有了点个人的感悟:
-级别1寻找数据可能优先从缓冲区找;级别2相当于不能读到缓冲区内容;级别3可能相当于在开启事务前对表做了个快照?级别4应该就是直接上了把互斥锁,同一时刻只能一个事务读写。
-
Java Database Connectivity Java语言操作数据库
-导入驱动jar包
-① 新建libs目录
-② 把jar包复制到libs目录下
-③ 右键libs目录 add as library
-注册驱动
-获取数据库连接对象 Connection
-定义sql语句
-获取执行sql语句的对象 Statement
-执行sql,接收返回的结果
-处理结果
-释放资源
-public class JdbcDemo1 { |
优化版【增加try-catch-finally】:
-public class JdbcDemo1 { |
目的是告诉程序该使用哪一个数据库驱动jar包
-在快速入门中,我们使用这一行来注册驱动:
-Class.forName("com.mysql.jdbc.Driver"); |
表面上看跟DriverManager类可以说是毫无关系。
-但其实,类加载器加载类的时候,其实是会自动执行类中的静态代码块的。Driver类中有一段静态代码块如下:
-static { |
可见,注册驱动其实主要任务是由DriverManager类干的。这个静态块仅仅用于简化代码书写。
---注意:mysql5之后的版本,这一步注册驱动可以省略。
--
配置文件里自动帮你注册了。我想原理应该是让本文件的类自动加载。
-
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/helloworld","root","root"); |
url的语法:”jdbc:mysql://IP地址:端口号/数据库名称”
-数据库连接对象。
-Statement createStatement() throws SQLException; |
void setAutoCommit(boolean autoCommit) throws SQLException; |
设置参数为false即开启事务。也即关闭自动提交。
-void commit() throws SQLException; |
void rollback() throws SQLException; |
--The object used for executing a static SQL statement and returning the results it produces.执行静态sql
-
//执行任意语句 |
封装查询结果集。
-具体取数方法就是类似迭代器原理。next移动迭代器指针,getXxx()方法,Xxx是数据类型,得到该行表记录中对应列对应数据类型的值。可以传入列数或者列名。
-boolean next() throws SQLException; |
使用实例:
-public static Collection<Client> query(){ |
注意:
-这东西也得Close。
-如果想做“查询到了结果则返回true”这样的操作,不应该使用这样的代码:
-if (resultSet != null) return true; |
而应该这样:
-return resultSet.next(); |
//An object that represents a precompiled SQL statement. |
Statement的子类。可以用来解决sql注入问题。
-它是预编译的sql语句,也即sql语句中的参数使用“?”占位符,需要传入参数。
-如下面的验证密码程序。键盘输入账号密码,从数据库查询该用户是否存在。
-public class Login { |
它更安全且效率更高。
-public class JDBCUtils { |
public static Collection<Client> query(){ |
使用Connection对象的管理事务的方法。
-public class Account { |
其实就是上面的JDBC中的Connection的对象池。
-非常简单,就是改一下Connection的获取,写一下xml就行。
-固定放在src目录下。名字必须为c3p0-config.xml或者c3p0.properties
-<c3p0-config> |
可以注意到,xml文件里面可以保存多套配置,比如上面的示例代码就保存了两套配置,default-config和name=”otherc3p0”的config。
-ComboPooledDataSource有一个含参构造器:
-public ComboPooledDataSource(String configName) { |
就可以传入config的名称指定要用的配置信息。
-DataSource cpds = new ComboPooledDataSource(); |
Druid的配置文件可以放在任意路径下,随便取名字。因为到时候需要指定配置文件。使用的是Properties文件。
-driverClassName=com.mysql.jdbc.Driver |
//导入配置文件 |
一般使用的时候还是会自定义一个工具类的
-import com.alibaba.druid.pool.DruidDataSourceFactory; |
使用同上的JDBCUtils
-Spring框架对JDBC的简单封装,提供JDBCTemplate对象。
-jdbcTemplate.update("update usr set money = ? where uname = ?",10,"Mary"); |
public static void main(String[] args) throws Exception { |
提供了三种方法。
-将得到的结果(只能是一行)封装为一个Map<String,Object>,其中key为列名,value为该行该列的值。
-如果得到的结果不为1行(=0 or >1),会抛出异常。
-JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource()); |
将得到的结果封装为List<Map<String,Object>>,其中一个Map为一行,多个Map表示多行,存储在List中。
-List<Map<String, Object>> res = jdbcTemplate.queryForList("select * from usr"); |
可以把查询回的结果封装为自己想要的对象而不是Map。如示例就封装为了Client对象。
-可以看到,里面的包装内容还是得自己写,有点麻烦。
-JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource()); |
使用包装好的BeanPropertyRowMapper类。
-JdbcTemplate jdbcTemplate = new JdbcTemplate(JDBCUtils.getDataSource()); |
注意:
-返回查到的某个东西。可以用于聚合函数的查询。
-int money = jdbcTemplate.queryForObject("select money from usr where uname = 'Mary'",Integer.class); |
JavaWeb:
-软件架构:
-B/S架构详解
-资源分类:
-我们要学习动态资源,必须先学习静态资源!
-静态资源:
-
|
布局
-页面布局使用table标签。这点让我感觉非常新奇。
-而且表格布局可以嵌套,也即每一行可以是一个新的表格。
-图片适应屏幕宽度
-只需在img标签加个width=”100%”的属性即可。如:
-<img src="./image/top_banner.jpg" width="100%"> |
表项中的数据要被提交的话,必须指定其名称
-也即一定要有属性name。
-关于from的属性
-一般都这么写
-
|
|
*{ |
1. 创建:
- 1. var fun = new Function(形式参数列表,方法体); //忘掉吧
- 2. function 方法名称(形式参数列表){
- 方法体
- }
-
- 3. var 方法名 = function(形式参数列表){
- 方法体
- }
-2. 方法:
-
-3. 属性:
- length:代表形参的个数
-4. 特点:
- 1. 方法定义是,形参的类型不用写,返回值类型也不写。
- 2. 方法是一个对象,如果定义名称相同的方法,会覆盖
- 3. 在JS中,方法的调用只与方法的名称有关,和参数列表无关
- 4. 在方法声明中有一个隐藏的内置对象(数组),**arguments**,封装所有的实际参数
-5. 调用:
- 方法名称(实际参数列表);
-
-/** |
特点:全局对象,这个Global中封装的方法不需要对象就可以直接调用。 方法名();
-方法:
encodeURI():url编码
decodeURI():url解码
encodeURIComponent():url编码,编码的字符更多
decodeURIComponent():url解码
parseInt():将字符串转为数字
-eval():讲 JavaScript 字符串,并把它作为脚本代码来执行。
-var str = "http://www.baidu.com?wd=传智播客"; |
getElementById(); |
createAttribute(name); |
removeAttribute(); |
说了树结构后,这个就好理解多了。
-
|
老师标答值得学习借鉴的点:
-//使用innerHTML添加 |
|
|
浏览器对象模型,将浏览器各个组成部分封装成对象。
-Window对象包含DOM对象。
-组成:Window、Navigator、Screen、History、Location
-不需要创建,直接用window.使用,也可以直接用方法名。比如alert
-与弹出有关的方法
-alert:弹出警告框; confirm:确认取消对话框。确定返回true;prompt:输入框。参数为输入提示,返回值为输入值。
-与开关有关的方法
-close:关闭调用的window对象的浏览器窗口;open:打开新窗口,可传入URL,返回新的window对象
-定时器
-//只执行一次 |
//一次性定时器 |
获取其他BOM对象
-history、location、navigator、screen
-获取DOM对象
-document
-
|
刷新
-location.reload方法
-设置或返回完整的url
-location.href属性
-
|
web前端框架
-基本模板:
-
|
实现依赖于栅格系统。
-将一行平均分成12个格子,可以指定元素占几个格子
-可以感受到,其实跟我们之前那个纯纯HTML做页面的思想是差不多的,都是把整个页面看做一个表,表有很多行,每行有不同的格子。
-容器分类:
-设备代号:
-
|
看文档。
-
|
*{ |
这东西写了我还挺久的。。。不过收获也挺多。
-text-align
-是一个css属性,我觉得挺好用的(。我不知道它精确是什么意思,但我发现它好像有种能让该元素下的子元素水平居中的效果。
-关于“容器”的理解
-上面说过,Bootstrap有个容器的概念,跟我们上面纯HTML的表格概念其实是很类似的。
-HTML的容器是表格标签,Bootsrap的容器是container-fluid和container类的标签。
-与HTML的表格相同,“容器”也是可以嵌套的。这点在本案例体现为一下两点:
-① container中可以嵌套container-fluid。
- 案例中,页首-轮播图和页尾这两段是两边不留白的,轮播图-页尾这段是两边留白的。所以,我们就可以让整体为一个container容器,中间一段再用container-fluid容器包装起来。也即:
-<body> |
注意,此处不要作死为了优雅统一性这样写:
-<div class="row container"></div> |
也即多加一个row类。要不row的属性会覆盖掉container的。
-② 对于“col-md-4”这些的理解
-在做这样的包含row-span元素的行时,之前的解决方案是采用表格嵌套。同样的,这里也可以采用容器嵌套。而此时,列的书写方式就比较特殊了。
-<div class="row"> |
其实是非常直观的,相信以后你看到这段应该也能理解(。提示一点,栅格系统其实好像是相对于父类的。也就是说,不是“把整个页面分成12个格子”,而是,“把父类占有的空间分成12个格子”。
-关于hr标签
-使用css改颜色时应该写background: orange;
而不是color: orange;
。
xml叫做可扩展标签语言。它的全部标签都是自定义的。
- 1. xml文档的后缀名 .xml
- 2. xml第一行必须定义为文档声明
- 3. xml文档中有且仅有一个根标签
- 4. 属性值必须使用引号(单双都可)引起来
- 5. 标签必须正确关闭
- 6. xml标签名称区分大小写
-
-
|
常见属性:version[必须]、encoding、standalone[取值为yes和no,yes为依赖其他文件]
-id属性值唯一
-这种要转义来转义去的显然很麻烦。所以就需要用到CDATA区。
-CDATA区的文本会被原样展示。
-<code> |
只能写约束文件内的标签
-//students标签里可以包含若干个student标签 |
//外部引入 |
或者可以直接在xml内部写:
-
|
<student number="s001"> |
|
|
它这意识就是,每个schema文件都要起一个别名,比如xsi:schemaLocation="http://www.itcast.cn/xml student.xsd"
这行代码实际上就是把student.xsd
的别名起为了http://www.itcast.cn/xml
。
为什么要起名呢?这就类似于命名空间这种东西,你要用到一个标签需要指明这个标签从哪来的,比如std::vector
,Class.toString
这种。这种命名空间在xml里叫前缀。所以,实际上完整写法应该是<http://www.itcast.cn/xml:students>
。
但这样显然太麻烦了,别名一般都是这种网址,写起来太长了。所以我们选择给别名起别名,设置方法为xmlns:a="http://www.itcast.cn/xml"
,这样一来,以后就不用写<http://www.itcast.cn/xml:students>
,只用写<a:students>
了。
但是如果每个都写一个前缀还是有点难顶。所以就引入了一个空前缀。这样写<students>
这样没有前缀的标签,就相当于从空前缀那个命名空间里拿出来的了。当然如果有多个命名空间,还是得区分一下的。
解析方式有两种方法。
-将标记语言文档一次性加载进内存,形成DOM树
-操作方便,可以进行CRUD所有操作;但占内存
-逐行读取,基于事件驱动
-不占内存;但只能读取
-主要学习Jsoup。
-跟前面html的DOM是差不多的。
-public class JsoupDemo { |
Jsoup:工具类,可以解析html或xml文档,返回Document
-Document:文档对象。代表内存中的dom树
-Elements:元素Element对象的集合。可以当做 ArrayList
Element:元素对象
-这一点很好理解。因为Document和Element对象的获取元素方法都继承自Node结点,本意就是获取子元素对象。只不过Document是根节点,所以就变成了获取所有元素对象。
-Node:节点对象
-使用选择器selector
-其实语法格式跟css的那个选择器差不多。
-/** |
使用XPath
-XPath:xml路径语言。
- -/** |
Tomcat是Java相关的web服务器软件。
-启动
省流:看系统环境变量有没有CATALINA_HOME这一项,并且看这个CATALINA_HOME的值是否与当前版本安装路径相符合。
-我电脑上本来也有了一个tomcat,只不过跟老师版本不一样。我把这两个都安到同一个目录了。然后我启动了老师版本,却发现输入localhost:8080没有任何响应。我首先去看了一下tomcat的config下的server.xml,发现端口号确实是8080没问题。然后试图访问localhost,发现没有响应,故推测是此处发生了问题。因而我上网按照该教程做了一遍:
-127.0.0.1 拒绝了我们的连接请求–访问本地IP时显示拒绝访问
-我重启电脑后,再次启动老师版本,发现还是不行。这时我开始怀疑是否我的tomcat没有正常启动,或者是否是因为8080这个端口号冲突了。所以我又找了一下如何查看端口号占用情况:
- -用netstat -a
命令即可。我便发现,在我开着tomcat的情况下,8080这个端口没有被使用。说明好像启动不大正常。于是我打开了另一篇回答:
按照它说的去查看日志文件。发现老师版本的tomcat下的log目录为空。我就去我本安装的版本下的log目录去看了,惊奇地发现,原来我在使用老师版本的tomcat时,tomcat用的是老版本的log目录。也就是说,很有可能config目录也是用的老版本的。我去查看老版本的config,发现端口是8888。于是我把老师版本的tomcat卸载了,去访问localhost:8888,成功力。
-我探寻了以下原因,发现tomcat的startup里面如此写道:
-if not "%CATALINA_HOME%" == "" goto gotHome |
这一段大概是在找到tomcat这个软件的位置。如果我们在环境变量里面设置了CATALINA_HOME,那么就会直接把软件位置定位到CATALINA_HOME的值的地方,随后之后的逻辑都在那边执行。
-我发现我确实设置了这个CATALINA_HOME,并且:
-它的值是我电脑原本有的老版本的目录!
-故而,这也就说明了为什么老师的版本不去用自己的log,不去用自己的config,而用的是我电脑上的老版本的log,config了。。。
-部署项目的方式:
-直接将项目放到webapps目录下即可。 * /hello:项目的访问路径–>虚拟目录 * 简化部署:将项目打成一个war包,再将war包放置到webapps目录下。
-配置conf/server.xml文件
在<Host>
标签体中配置
<Context docBase="C:\aWorkSpace\Projects\Java\JavaWeb" path="/web" />
然后之后访问时输入`localhost/web/JavaWeb.html`即可
-
-* docBase:项目存放的路径
-* path:虚拟目录
-
-<Context docBase="C:\aWorkSpace\Projects\Java\JavaWeb" />
注意,该方法是热部署的。也就是说,可以不关闭服务器的情况下,去增删xml文件,会马上变化,而不是像上面两种方式一样重启生效。
-
-项目都存放在webapp里。打开webapp中的任一个。
-WEB-INF下是动态资源,也就是Java控制的一些文件【大概这个意思】。有这个文件夹的项目是动态项目。
-WEB-INF以外的都是静态资源。
-然后等着它开始下载就行了。
-最后的目录结构:
-如果java或者resources目录没有,自己建就行。
-1.
- -2.
-还有另一种更便捷的方式,就是直接添加maven的tomcat插件。在pom.xml文件里加入此段:
-<build> |
即可,可用alt+insert自动补全。
-这里我出现了一个飘红报错问题,用这个可以解决:
-maven学习 & Plugin ‘org.apache.tomcat.maven:tomcat7-maven-plugin:2.2’ not found报错解决【问题及解决过程记录】
-然后,右键项目就可以run了:
-如果没有此选项,就去下载maven helper插件。
-run-edit configuration-tomcat
-启动服务器时控制台前几句输出有一句这样的。对应目录下的就可以找到tomcat配置文件。
-server applet运行在服务器端的小程序
-servlet是java编写的服务器端的程序,运行在web服务器中。作用:接收用户端发来的请求,调用其他java程序来处理请求,将处理结果返回到服务器中
-servlet是接口,定义了Java类被tomcat执行、被浏览器访问的规则。
-这里的配置用的是注解,具体原理在第一部分的JavaSE基础里有详细描述了。
---使用maven创建web项目见上面的tomcat-tomcat集成到IDEA-使用maven创建web项目
-
--如果已经导入依赖坐标却还未生效,就点击右侧侧边栏的maven刷新。
- -
Servlet的init方法只执行一次,一种Servlet在内存中只存在一个对象,Servlet是单例的。因而,当多线程同时访问同一个Servlet对象时,就会产生线程安全问题。所以有需要的话,就要采取手段保障Servlet类的线程安全性。
-为了简化开发,我们可以用提供的servlet的实现类。
-除了service方法之外的方法,差不多都只做了空实现。所以只需写service方法即可。
-
|
比如httpservlet,就只用重写里面的doGet和doPost两个方法就行。
-
|
这两个方法的区别就是,当使用get方式提交表单,就会执行第一个方法;使用post则会执行第二个方法。
-比方说post时:
-网页代码如下(放在webapp目录下)
-
|
servlet代码同上。
-最终在网页中点击提交
-会跳转到\demo页面【也即servlet的访问路径】,并且在console打印“post!!!”
---为啥会这样呢?
-之前在讲表单的时候说过,form的action属性代表着提交时这个表单会提交给谁,值为一个URL。所以,这里action的值设置为Servlet的路径,意思就是把表单数据发送给了Servelet,由于使用的是post方式,因此触发了Servlet的doPost方法。Servlet对得到的数据进行各种处理,并且通过req和resp进行交互。
-
--为什么此处写的是“\demo”这样的路径?
-事实上这是一个相对路径。
--
部署的根路径可以在 run-edit configuration-tomcat-deployment中找到。
-
之所以这两种方法需要分别处理,是因为在Servlet的service方法中,其实是要对req对象进行参数分解,这两种方法分解方式不一样。
-按照以往,我们需要这样写
-public void service(ServletRequest servletRequest, ServletResponse servletResponse) { |
就类似于可以这么写:
-public void service(ServletRequest servletRequest, ServletResponse servletResponse){ |
于是最后就融合入httpservlet了。
-一个Servlet可以定义多个访问路径 : @WebServlet({“/d4”,”/dd4”,”/ddd4”})
-路径定义规则:
-http://localhost/webdemo4_war/*.do
。概念:Hyper Text Transfer Protocol 超文本传输协议
-传输协议:定义了客户端和服务器端通信时发送数据的格式
-特点:
---如果说域名是ip地址的简化表示,ip地址又表示着一台主机,那么使用http协议访问一个网址,相当于访问一台主机,并且端口号为80.
-
历史版本:
-客户端发送给服务器端的消息
-数据格式:
-请求行
请求方式 请求url 请求协议/版本
GET /login.html HTTP/1.1
请求头:客户端浏览器告诉服务器一些信息
请求头名称: 请求头值
常见的请求头:
-User-Agent:浏览器告诉服务器,我访问你使用的浏览器版本信息 * 可以在服务器端获取该头的信息,解决浏览器的兼容性问题
-Accept:可以支持的响应格式
-Accept-language:可以支持的语言环境
-Referer:http://localhost/login.html * 告诉服务器,我(当前请求)从哪里来?
-Connection:连接是否活着
-请求空行
空行,就是用于分割POST请求的请求头,和请求体的。
请求体(正文):
-字符串格式:
-//请求行 |
响应消息:服务器端发送给客户端的数据
-数据格式:
-响应行
-组成:协议/版本 响应状态码 状态码描述 HTTP/1.1 200 OK
响应状态码:服务器告诉客户端浏览器本次请求和响应的一个状态, 状态码都是3位数字. 分类:
-1xx:服务器接收客户端消息,但没有接收完成,等待一段时间后,发送1xx多状态码,询问是否还要继续发
-2xx:成功。代表:200
-3xx:重定向。代表:302(重定向),304(访问缓存)
-需要自动重定向到另一个C去
-发现资源未变化且本地有缓存
-4xx:由客户端造成的错误
-代表:
-404(请求路径没有对应的资源,可能路径输错了)
-405:请求方式没有对应的doXxx方法
-当我们在Servlet中未重写doXXX方法,就默认不能用此方法进行访问。因为doXXX方法的默认实现为:
-String protocol = req.getProtocol(); |
5xx:服务器端错误。
-代表:500(服务器内部出现Exception)
-int i = 3/0; |
响应头:
-格式: [头名称 : 值]
-常见的响应头:
-Content-Type:服务器告诉客户端本次响应体 数据格式以及编码格式
-浏览器依照编码格式来对该页面进行解码。
-Content-disposition:服务器告诉客户端以什么格式打开响应体数据
-响应空行
-响应体:传输的数据
-字符串格式:
-//响应行 |
ServletRequest(I) - HttpServletRequest(I) - RequestFacade(C)[tomcat创建]
-获取请求方式 POST
-String getMethod() |
获取虚拟目录 /webdemo
-String getContextPath() |
获取Servlet路径 /demo1
-String getServletPath() |
获取get方式请求参数 name=zhangsan
-&分割每个键值对
-String getQueryString() |
获取请求URI和URL
-// /webdemo/demo1 |
--URL:统一资源定位符 : http://localhost/day14/demo1 中华人民共和国
-
URI:统一资源标识符 : /day14/demo1 共和国URI的代表范围更大
-
获取协议及版本 HTTP/1.1
-String getProtocol() |
获取访问的客户机的IP地址
-String getRemoteAddr() |
通过请求头的名称获取请求头的值
-String getHeader(String name) |
获取所有的请求头名称
-Enumeration<String> getHeaderNames() |
返回的是一个迭代器
-public class Servletdemo2 extends GenericServlet { |
这些请求头名称正是上面的键值对里的键。
-request将请求体中的数据封装成了流。如果数据是字符,那就是字符流;是视频这种的字节,那就是字节流。
-* 步骤:
- 1. 获取流对象
- * BufferedReader getReader():获取字符输入流,只能操作字符数据
- * ServletInputStream getInputStream():获取字节输入流,可以操作所有类型数据
- 2. 操作流获取数据
-
-
|
请求体中键值对会在一行里,用&分割
---获取时中文乱码
--
-- -
get方式:tomcat 8 已经将get方式乱码问题解决了
-- -
post方式:会乱码
--* 解决:在获取参数前,设置流的编码: - -
-
request.setCharacterEncoding("utf-8");
这里的请求参数应该是指上面Post的请求体、Get的请求行里的参数,请求头里的参数是获取不到的。
-根据参数名称获取参数值
-String getParameter(String name) |
如 username=zs&password=123,getParameter(“username”)会得到zs。
-根据参数名称获取参数值的数组
-String[] getParameterValues(String name) |
如 hobby=xx&hobby=game,会得到{xx,game}
-获取所有请求的参数名称
-Enumeration<String> getParameterNames() |
取所有参数的map集合
-Map<String,String[]> getParameterMap() |
在服务器内部资源跳转。
-AServlet做了一部分事情,把剩余的事情交给BServlet去做
-步骤:
-通过request对象获取请求转发器对象
-RequestDispatcher getRequestDispatcher(String path) |
使用RequestDispatcher对象来进行转发
-requestDispatcher.forward(ServletRequest request, ServletResponse response) |
|
特点:
-接力工作的两个Servlet可以通过request对象进行数据通信。
-* 方法:
-
-存储键值对
-void setAttribute(String name,Object obj) |
获取值
-Object getAttitude(String name) |
移除键值对
-void removeAttribute(String name) |
ServletContext getServletContext() |
--要求:
-1.编写login.html登录页面
-
username & password 两个输入框
2.使用Druid数据库连接池技术,操作mysql,day14数据库中user表
3.使用JdbcTemplate技术封装JDBC
4.登录成功跳转到SuccessServlet展示:登录成功!用户名,欢迎您
5.登录失败跳转到FailServlet展示:登录失败,用户名或密码错误
![屏幕截图 2023-01-02 235207](./JavaWeb/屏幕截图 2023-01-02 235207.png)
---错误历程
--
-- -
lib目录位置错误
-NoClassDefFoundError解决方案一开始lib目录没放进web-inf,通过此文章得知错误为包未引入,再由下面这篇文章得知lib目录放置错误
-JDBC Template报错:java.lang.ClassNotFoundException: org.springframework.jdbc.core.RowMapper
-- -
druid.properties文件位置错误
-报错
--
java.lang.NullPointerException at java.util.Properties$LineReader.readLine(Properties.java:434)
,报错位置在
-pro.load(JDBCUtils.class.getClassLoader().getResourceAsStream("druid.properties"));
由文章
-关于java.lang.NullPointerException at java.util.Properties$LineReader.readLine(Properties.java:434)问题
-回忆到,由于是使用类加载器获取文件流,故而要求druid.properties文件应该放在resource文件下。对于以前的项目,resource文件都默认是src文件夹。
-但是这次放在src目录下还是不行。定睛一看它web项目文件结构中有一个硕大的resources……放在下面果然就好了。
-
driverClassName=com.mysql.jdbc.Driver |
|
|
|
|
--关于BeanUtils
-BeanUtils工具类,简化数据封装, 用于封装JavaBean的
--
-- -
JavaBean:标准的Java类
--1. 要求: - 1. 类必须被public修饰 - 2. 必须提供空参的构造器 - 3. 成员变量必须使用private修饰 - 4. 提供公共setter和getter方法 - 2. 功能:封装数据 -
- -
概念:
- 成员变量:
-
属性:setter和getter方法截取后的产物-例如:getUsername() --> Username--> username -
- -
方法:
--1. setProperty() -1. getProperty() -1. populate(Object obj , Map map): -
将map集合的键值对信息,封装到对应的JavaBean对象中
-
原封不动地照搬了:第二部分-数据库连接池-Druid-定义工具类 部分的代码。
-public class UserDao { |
设置响应消息。
-设置状态码
-setStatus(int sc); |
setHeader(String name, String value) |
以流的方式传输数据。
-使用步骤:
-获取输出流
-字节输出流
-ServletOutputStream getOutputStream() |
字符输出流
-PrintWriter getWriter() |
使用输出流,将数据输出到客户端浏览器
-资源跳转的一种方式。
-
|
|
输出: |
重定向的这几行代码其实是可以简化的:
-/* 重定向 */ |
可以简化为:
-resp.sendRedirect("/practice_war/demo2"); |
--关于req对象不一样,但hashcode值相同的解释:
-hashcode很大程度与对象内存空间相关,与对象的具体内容没什么关系。两个对象拥有相同的hashcode有可能只是因为存储的内存空间位置大小都相同导致的。所以是因为两次的req对象都占用了同一个内存空间【JVM调度问题】,所以才让hashcode值相同。这两个对象实质上是不一样的。
-
重定向的特点(与请求转发完全相反):
-路径写法:
-相对路径:通过相对路径不可以确定唯一资源
-绝对路径:通过绝对路径可以确定唯一资源
-如:http://localhost/day15/responseDemo2 /day15/responseDemo2
-<form action="/webdemo4_war/check" method="post"> |
以/开头的路径
-规则:判断定义的路径是给谁用的?判断请求将来从哪儿发出
-给客户端浏览器使用:需要加虚拟目录(项目的访问路径)
-比如说在页面中弄了个a标签,将来是要给客户端点的,那么这个a标签的href就要用绝对路径。
-再比如说重定向:
-//要填的是完整资源路径。 |
这个路径将来是给客户端将来要使用的路径,是客户端路径,所以要加虚拟目录。
-给服务器使用:不需要加虚拟目录
-比如说之前的请求转发
-
|
|
|
|
代表整个web应用,可以和servlet容器(服务器)通信
-ServletContext getServletContext() |
this.getContext(); |
MIME是在互联网通信过程中定义的一种文件数据类型
- * 格式: 大类型/小类型 text/html image/jpeg
-
-/* |
mime映射存在了服务器的xml文件中。
-使用案例:
-System.out.println(this.getServletContext().getMimeType("a.txt")); |
ServletContext是一个域对象,可以用来共享数据。
-ServletContext代表着服务器,因而它的生命周期跟随服务器关闭而灭亡。ServletContext可以共享所有请求的数据。也就是说,任何一次请求,任何用户,看到的ServletContext域都是同一个。
-这样大的效果也使得我们需要更加谨慎地使用它。一旦数据存入ServletContext域,就只会在服务器关闭后才会消亡,很耗内存。
-String getRealPath(); |
经测试发现,这东西只是起了一个字符串拼接的作用,是不会帮忙检查文件是否存在的。
-学到这我顺便看了看文件放在不同的地方最后应该如何访问:
-这是最终部署项目文件夹的结构:
-可以看到只有bcd被保留了。它们的目录要这样获取:
-
|
用户点击下载->请求发送给某个servlet,servlet修改response->tomcat响应用户,传递的图片资源按照response的方法打开
-说实话看了感觉有点难以下手,主要还是完全不知道html和servlet怎么交互造成的,看了老师讲解才有点恍然大悟。
-我们可以把a标签以重定向的角度去看。它会新建一个request,然后发送到它的href中的那个url。在此处我们将url设置为/practice_war/download?filename=1.jpg
,也即要以GET的方式发送给download,请求体为filename=1.jpg
。然后servlet执行结束后,就会将信息存储在resp中返回给tomcat,由tomcat发送给用户。
<body> |
思路:
-获取要下载的资源,并且将其输入到resp的stream中。
-有一点需要非常注意:
-resp.setContentType(this.getServletContext().getMimeType(path)); |
必须要在把资源输入到resp的stream前设置好,精确来说是调用sos.write
前设置好,不然无法起作用。
猜测是因为可能resp会根据disposition方式的不同而自动决策write的方式。
-
|
会话:一次会话中包含多次请求和响应。
-一次会话:浏览器第一次给服务器资源发送请求,会话建立,直到有一方断开为止
-功能:请求之间本来是相互独立的。将多次请求组织在一次会话中,就可以让请求之间进行数据的共享。
-方式:
-客户端会话技术 Cookie
-把数据存进客户端
-服务器端会话技术 Session
-把数据存进服务器端
-客户端会话技术,将数据保存到客户端
-使用步骤:
-代码
-
|
|
得到效果
-运行服务器,首先访问/demo,然后在同一个浏览器再次访问/demo2,就可以在控制台看到输出。
-这个过程发生了什么呢?
-首先,访问/demo就相当于建立了会话。/demo的Servlet获取到请求之后,在response中将cookie填入。
-保持浏览器窗口不变,会话也不变。
-再次访问/demo2,cookie信息自动保存在request对象中。/demo2的Servlet获取到请求之后,在控制台中打印输出了cookie。
-你看它那个API叫add,就知道数据结构差不多是个list,所以多次add就行。
-默认情况下,浏览器关闭则cookie就马上被销毁。
-如果需要持久化存储:
-cookie.setMaxAge(int seconds) |
参数:
-在tomcat 8 之前 cookie中不能直接存储中文数据,需要将中文数据转码——一般采用URL编码(%E3)
-在tomcat 8 之后,cookie支持中文数据。
-假设在一个tomcat服务器中,部署了多个web项目,那么在这些web项目中cookie能不能共享?
-默认情况下cookie不能共享
-共享方法:
-setPath(String path)
:设置cookie的获取范围。默认情况下,设置当前的虚拟目录
如果要共享,则可以将path设置为”/“
-不同的tomcat服务器间cookie共享问题?
-比如说:
-setDomain(String path)
:如果设置一级域名相同,那么多个服务器之间cookie可以共享
(setDomain(".baidu.com")
,那么tieba.baidu.com和news.baidu.com中cookie可以共享)
特点:
-cookie存储数据在客户端浏览器
-因而它相对不安全
-浏览器对于单个cookie 的大小有限制(4kb) 以及 对同一个域名下的总cookie数量也有限制(20个)
-作用:
-cookie一般用于存出少量的不太敏感的数据
-在不登录的情况下,完成服务器对客户端的身份识别
-比如说,以不登录情况下对某个网页进行属性设置,你下次打开的时候属性设置依然在,这是因为你的属性设置的cookie在设置后被存入到你的电脑中,下次访问该网页发出请求,服务器端就能根据请求中cookie里的属性设置信息来做出响应了。
-需求:
1. 访问一个Servlet,如果是第一次访问,则提示:您好,欢迎您首次访问。
2. 如果不是第一次访问,则提示:欢迎回来,您上次访问时间为:显示时间字符串
public class ServletDemo extends HttpServlet { |
服务器端会话技术,在一次会话的多次请求间共享数据,将数据保存在服务器端的对象中。HttpSession
比如说购物网站的购物车这种,就会存在session。想想也是(
-session用于存储一次会话的多次请求的数据,存在服务器端
-比如说,当我们做重定向的时候,就可以选择用session共享数据(会话域)而非使用ServletContext
(此范围过大)
session可以存储任意类型,任意大小的数据
-session与Cookie的区别:
-获取HttpSession
对象:
HttpSession session = request.getSession();
使用HttpSession
对象:
Object getAttribute(String name) |
客户端不关闭,服务器关闭后,两次获取的session是同一个吗?
- * 在服务器正常关闭之前,将session对象序列化到硬盘上
-
- * 具体是会放在这里:
-
- ![image-20230223104447097](./JavaWeb/image-20230223104447097.png)
-
-* session的活化:(反序列化
- * 在服务器启动后,将session文件转化为内存中的session对象即可。
-
-我想,cookie应该在这点上不会像session这么做,因为cookie本质上是保存在客户端的数据,按理来说服务器端把cookie发出去之后就可以销毁了,在服务器序列化一点意义都没有。
-
-销毁时间
-服务器关闭
-session对象调用invalidate() 。
-session默认失效时间 30分钟
选择性配置修改
可以在每个项目的子配置文件(如下图)或者总的项目的父配置文件apache-tomcat-8.5.83\conf\web.xml
中配置
<session-config> |
--需求:
--
-- 访问带有验证码的登录页面login.jsp
-- 用户输入用户名,密码以及验证码。
--
-- 如果用户名和密码输入有误,跳转登录页面,提示:用户名或密码错误
-- 如果验证码输入有误,跳转登录页面,提示:验证码错误
-- 如果全部输入正确,则跳转到主页success.jsp,显示:用户名,欢迎您
-
我们可以在服务器端使用session存储password和username的map,存储验证码图片编号和图片的map,然后用cookie携带验证码图片编号,在req中存储用户名和密码。
-感觉我上面的思路是没有充分利用到session的性质,仅仅把它作为在服务器端存储数据的工具,
-“在服务器端存储password和username的map,存储验证码图片编号和图片的map,然后用cookie携带验证码图片编号,在req中存储用户名和密码。”
-这样也依然成立,跟session没半毛钱关系。我们可以这样使用session:
-那么在这里为什么不用Cookie而使用session呢?大概是因为cookie不安全罢(慌乱)
-<html lang="en"> |
|
|
老师的写法是将错误信息直接写在原登录界面,和我的略有不同:
-// in loginServlet |
// in login.jsp |
以及success.jsp
仅以成功为例
-
|
--现在都用 Thymeleaf ,更符合 MVC 的执行过程,也没有 JSP 这种耦合杂乱的页面代码,但是模板引擎的思路大致相同,还是可以看一看的
-
改动之后无需重启服务器,刷新界面即可。
---关于热更新的机制可以看看这篇文章,水平有限还看不懂就先放在这了:
- -
JSP(Java Server Pages) Java服务器端页面,用于简化书写
-可以理解为:一个特殊的页面,其中既可以定义html标签,又可以定义java代码
-比如说,上一个案例的Servlet代码就可以直接写入到JSP中,而且response和request这些对象可以直接用
-<%@ page import="java.text.SimpleDateFormat" %> |
最终效果:
-JSP本质上是Servlet
-JSP定义Java代码的方式
-<% 代码 %>
-定义的java代码,在service方法中。service方法中可以定义什么,该脚本中就可以定义什么。
-也即最后会构成Servlet体
-<%! 代码 %>
-定义的java代码,在jsp转换后的java类的成员位置。可以是成员变量,或者是成员方法。
-注:最好不要在Servlet中定义成员变量,否则容易引发线程安全问题。
-<%= 代码 %>
-定义的java代码,会输出到页面上。输出语句中可以定义什么,该脚本中就可以定义什么。
-比如说可以用来输出某个变量的值。注意这东西由于本质上是写在Servlet的service方法中的,因而当成员变量和service方法的局部变量重名,会依据就近原则优先使用局部变量的值。
-也就是jsp开头那些东西,比如说这个:
-<%@ page contentType="text/html;charset=UTF-8" language="java" %> |
用来配置jsp的资源页面信息
-contentType="text/html;charset=UTF-8" |
* <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
- * prefix:前缀,自定义的。之后就可以用`<c:XXX>`了。相当于什么std::。
-
-但是注意一点
-//获取流对象之前,设置流的默认编码:ISO-8859-1 设置为:GBK |
这样做在Servlet不会导致中文乱码,但JSP不行,这个大概是因为两者原理不一样。
-Servlet的中文乱码:
-JSP的:
-Servlet乱码是因为客户端和response请求体编码不一致,JSP乱码与JSP的原理有关,是只跟服务器端有关
---编译jsp有以下几个步骤:
-
(1)把jsp转化为java源码。pageEncoding=xxx指定以xxx编码格式读取jsp文件,因此,jsp文件的编码格式应与pageEncoding值一致。
(2)把java源码编译为字节码,即.class文件。转化后的java源码为utf-8编码格式,字节码也为utf-8编码,我们无需干预。
(3)执行.class文件。在此过程,需向浏览器发送中文字符,contentType=xxx指定了jsp以xxx编码显示字符。也就是在浏览器中查看页面编码,其值为contentType指定的编码。因此,在1、3环节,**只要指定一致的编码格式(jsp文件编码格式=pageEncoding=contentType)**,即可保证jsp页面不出现乱码。
-
举例:jsp文件以utf-8格式编写,那么pageEncoding=utf-8, contentType=utf-8,就保证了jsp页面不出现乱码。
————————————————
版权声明:本文为CSDN博主「liuhaidl」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/liuhaidl/article/details/84012372
指定方法是在JSP开头添加:
-<%@ page pageEncoding="UTF-8"%> |
在jsp页面中不需要获取和创建,可以直接使用的对象。
-jsp一共有9个内置对象。
-request HttpServletRequest
一次请求访问的多个资源(转发)
response HttpServletResponse
响应对象
out: JspWriter
字符输出流对象。可以将数据输出到页面上。和response.getWriter()类似
pageContext PageContext
当前页面共享数据,还可以获取其他八个内置对象
session HttpSession
一次会话的多个请求间
application ServletContext
所有用户间共享数据
page Object
当前页面(Servlet)的对象,相当于this
config ServletConfig
Servlet的配置对象
exception Throwable
异常对象。只在page指令的isErrorPage
为true的情况下才能使用此对象。
其中,
-这四个为用来共享数据的域对象
-将程序分成三个部分,分别是M-V-C。
-服务器将接收的请求给控制器处理,控制器控制model完成必要的运算,model把算出的东西返回给控制器,控制器再把数据交给视图展示,数据最终就回到了浏览器客户端。
-这就算是一个微型CPU了吧,控制器就是CU,模型就是ALU,也许客户端和视图什么的可以视为IO接口。
-优缺点:
-优点:
-缺点:
-那么,我们可以知道,jsp就只需负责数据的展示了。那怎么展示数据呢?这就需要用到jsp的几个技术了:
---注意,servlet3.0以来默认关闭el表达式解析,需要手动在page上加属性打开,详见 jsp文件中的el表达式失效问题解决
-
Expression language,替换和简化jsp上java代码的书写
-语法:${表达式}
-jsp会执行里面的表达式,然后把结果输出。
-加反斜杠可忽略。
-使用场景:
-运算
- 1. 算数运算符: + - * / %
- 2. 比较运算符: > < >= <= == !=
- 3. 逻辑运算符: && || !
- 4. 空运算符: empty
- * 功能:用于判断字符串、集合、数组对象是否为null**或者**长度是否为0
- * `${empty 变量名}`: 判断字符串、集合、数组对象是否为null或者长度为0
- * `${not empty 变量名}`: 表示判断字符串、集合、数组对象是否不为null 并且 长度>0
-
-获取值
-el表达式只能从域对象中获取值
-语法:
-${域名称.键名}
:从指定域中获取指定键的值pageScope
–> pageContextrequestScope
–> requestsessionScope
–> sessionapplicationScope
–> application(ServletContext)${requestScope.name}
${键名}
:表示依次从最小的域中查找是否有该键对应的值,直到找到为止。案例
-这样一来,访问/demo
就能转发到index.jsp
,显示出属性值
Servlet
-protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { |
index.jsp
-<%@ page pageEncoding="UTF-8" isELIgnored="false" %> |
获取非字符串类型的值
-对象
-集合(List、Map等)
-学习目的:顺利过考试,以及获取基本的密码学知识,数学原理不重要
- -只有理论上意义
-实际上不可行
-你也是过渡阶段?
-逆元:
-比如说在G(7)中,2的逆元为4。
-也即,任意整数a,则存在x,a / 2 == a * 4 (mod 7),4为2模7的乘法逆元,记为 2(-1)(mod 7) = 4。
-求逆元的方法是求b^(m-2) mod m。如2^(5) mod 7 = 4。
-确实封闭且结合且单位元且逆元
-确实是环
-有限域就是阶为素数幂的域?
-确实,毕竟系数本身就是域了,除了没定义逆元外别的都满足。
-Nr=Nk的幂数x2
-具体算法详见PPT。
-可以关注下是怎么通过C矩阵求出这个固定多项式的:
-感觉也是类似对明文做的操作
-序列密码的密钥序列是随机的。
-确实,感觉相比上面的这笔就是换了个反馈函数,就达到了2^n-1的周期
-也就是说求最大公因子实际上可以只求共有素数因子
-看起来意思就是公钥完全明文,用的是用户的身份ID;私钥用户自己存着。
-这个角度很有意思,确实是名字一样原理相近,但是目的完全不一样:
-也就是中途会哈希两次吼。
-一样的话就是说明消息没被篡改
-都包含签名算法、验证算法、正确性证明、举例,详细看PPT吧。
-这个有点复杂,可以看看PPT。
-Expression language,替换和简化jsp上java代码的书写
+语法:${表达式}
+jsp会执行里面的表达式,然后把结果输出。
+加反斜杠可忽略。
+使用场景:
+运算
+ 1. 算数运算符: + - * / %
+ 2. 比较运算符: > < >= <= == !=
+ 3. 逻辑运算符: && || !
+ 4. 空运算符: empty
+ * 功能:用于判断字符串、集合、数组对象是否为null**或者**长度是否为0
+ * `${empty 变量名}`: 判断字符串、集合、数组对象是否为null或者长度为0
+ * `${not empty 变量名}`: 表示判断字符串、集合、数组对象是否不为null 并且 长度>0
+
+获取值
+el表达式只能从域对象中获取值
+语法:
+${域名称.键名}
:从指定域中获取指定键的值pageScope
–> pageContextrequestScope
–> requestsessionScope
–> sessionapplicationScope
–> application(ServletContext)${requestScope.name}
${键名}
:表示依次从最小的域中查找是否有该键对应的值,直到找到为止。案例
+这样一来,访问/demo
就能转发到index.jsp
,显示出属性值
Servlet
+protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { |
index.jsp
+<%@ page pageEncoding="UTF-8" isELIgnored="false" %> |
获取非字符串类型的值
+对象
+集合(List、Map等)
+向量处理器意思是一条指令可以同时处理多个数据元素(SIMD)(就类似于这几个数据元素组成了一个向量);多发射处理器可以同一时间并行多条指令。
+发射与流出
+在计算机体系结构中,”发射”和”流出”是与指令执行有关的两个重要概念,它们描述了处理器在执行指令时的不同阶段和行为。
+这两个概念都涉及到提高指令级并行性,但它们描述了处理器在执行阶段的不同方面。发射强调在同一时钟周期内同时发送多条指令,而流出强调在执行过程中的乱序执行策略。
+tensor 张量
+sparse tensor 稀疏张量
+异构计算
+指的是在同一系统中集成多种不同体系结构或架构的处理器和计算设备,以便更有效地处理各种类型的任务。这包括集成不同类型的中央处理单元(CPU)、图形处理单元(GPU)、加速器、协处理器等。异构计算的目标是充分发挥各种处理器的优势,以提高整体系统性能和能效。
+其关键概念有协处理器等等等。
+注意几点:
+注意,它的意思是LD、SD、DADDIU都只占1个时钟周期,ADD占2个
+感觉这么个例题下来,我就懂了循环展开的作用了
+这里的结构相关值得注意
+做这种题的套路是,需要明确它要求的时刻时的情况,并且依照以下规则判断即可:
+指令状态表
+流出
+无结构冲突、无WAW冲突
+如① 当MULT准备写回时,此时前两条L必定流出,然后后面的SUB、DIV、ADD都没有结构冲突和WAW冲突,所以全部流出。只不过ADD和DIV会卡在读操作数阶段
+② 由①可知全部流出
+读操作数
+操作数可用时完成该阶段
+如① 此时前三条必定完成。并且SUB也完成了,所以ADD也完成了读数阶段。只有DIV还在等待mul的结果
+② 此时大伙差不多都结了,没什么好说的
+执行
+纯纯的算术
+如① 除了除法别的都完了,没什么好说的
+② 全部都结了
+写结果
+不存在WAR则写入
+如① 前两个肯定完成了,然后SUB也结了,ADD存在WAR,所以最后是ADD和MUL没完成。
+② 除了DIV全部结了
+功能部件状态表
+记住这些字母的含义即可:
+寄存器状态表
+每个寄存器有一项,用于指出哪个功能部件将把结果写入
+3段流水
+流出
+没有结构冲突就流出,填进保留站
+一般有ADD1,ADD2,ADD3(加减),MUL1,MUL2(乘除),LD1,LD2(SL)
+具体填什么看操作数有没有就绪
+保留站有以下字段:
+Op:操作
+Qj,Qk:操作数保留站号
+Vj,Vk:源操作数值
+load的Vk保存偏移量
+Busy
+A:存放立即数字段 or 有效地址,仅用于load和store缓冲器
+Qi:寄存器状态表
+存放要写入它的保留站ID
+执行
+两个操作数就绪后就执行
+写结果
+计算完毕后由CDB传送
+这里不知道为什么LD2没有跟LD1同时完成?限制了一个时钟周期只流出一条指令吗
+这里可以注意其特点是结果一经算出全部写回
+通过换名避免了WAR,而不是像记分牌那样通过等待
+4段流水
+流出
+保留站&ROB都有空闲才流出
+一般有ADD1,ADD2,ADD3(加减),MUL1,MUL2(乘除),LD1,LD2(SL)
+具体填什么看操作数有没有就绪
+保留站有以下字段:
+Op:操作
+Qj,Qk:操作数保留站号
+Vj,Vk:源操作数值
+load的Vk保存偏移量
+Busy
+A:存放立即数字段 or 有效地址,仅用于load和store缓冲器
+Qi:寄存器状态表
+存放要写入它的保留站ID
+执行
+两个操作数就绪后就执行
+写结果
+ROB字段:
+指令类型
+目标地址
+目标寄存器/存储器单元地址
+数据值字段
+前瞻结果
+就绪字段
+结果是否就绪
+指令确认
+分支结果出来后确认
+注意,SD指令的0和R1有了就开始执行,不必等到F4有了再执行。。。
+具体步骤:
+看起来大概可能就有点类似树的概念,什么都不依赖的就放前面,然后依赖1层的依赖2层的之类的
+开发指令级并行ILP的方法
+流水线CPI
+实际CPI = 理想CPI + 停顿(结构/数据/控制冲突引起)
+理想CPI是衡量流水线最高性能
+IPC:每个时钟周期完成的指令数
+CPI:每个指令所需时钟周期数
+基本程序块:一串没有分支和跳转转入点的指令块
+解决冲突的方法之一是序列调度,不过对于跨块的调度(也即jump指令)会有影响
+相关:两条指令之间存在某种依赖关系
+只能部分(完全不难)在流水线中重叠执行
+类型:数据相关(真数据相关)、名相关、控制相关
+约定先执行i再执行j
+数据相关
+定义:j使用i的结果,也即先写后读
+具有传递性
+反映数据流动关系,即如何从生产者流动到消费者
+数据相关不能并行,需要插入暂停解决冲突
+解决方法
+保持相关但避免冲突
+调度
+变换代码消除相关关系
+检测方法
+流经寄存器时直观;流经存储器复杂
+名相关
+分类
+解决方法:换名技术,可以编译器静态实现 or 硬件动态实现
+相关问题
+寄存器换名可以消除WAR和WAW冲突
+数据冲突
+注意这里的命名,是按照正确顺序命名的。比如说RAW(read after write),写后读,正确次序就是i写入然后j再读,所以叫写后读。
+RAW(数据相关)
+也即i写j读
+WAW(输出相关)
+也即i写j写
+流水线发生条件:流水线不止一个段可以写操作、指令被重新排序
+5段流水线不会发生,因为只会在WB阶段写寄存器
+WAR(反相关)
+也即i读j写
+流水线发生条件:有些指令写操作提前有些读操作滞后、指令被重新排序
+控制相关
+由分支指令引起
+这里可能意思是引出了多流出,所以会导致DIV和ADD同时流出,从而发生WAW。同理,可能的阻塞也会导致WAR。
+基本思想
+在没有结构冲突时,尽可能早地执行没有数据冲突的指令,实现每个时钟周期执行一条指令
+基本结构
+三张表:指令执行状态、功能部件状态、寄存器状态及数据相关关系
+指令状态表
+记录正在执行的各条指令的状态
+功能部件状态表
+记录各个功能部件状态,每项有以下字段:
+结果寄存器状态表
+每个寄存器有一项,用于指出哪个功能部件将把结果写入
+大概是这样的结果:n(寄存器数量) X m(功能部件数量) 的值为0 or 1的矩阵
+执行流程
+每条指令的执行过程分为4段(只考虑浮点计算)
+流出
+如果①所需功能部件空闲(结构冲突) ②其他正在执行指令目的寄存器与当前不同(WAW冲突),则流出
+读操作数
+记分牌监测操作数可用性,可用时通知功能部件从寄存器中读出源操作数开始执行(RAW冲突)
+写结果
+记分牌监测是否完成执行,若不存在or已消失WAR,则写入;存在,等待
+性能分析
+核心思想
+记录和检测指令相关,操作数一旦就绪立刻执行,把发生RAW的可能减到最小;
+通过寄存器换名消除WAR和WAW(上面的记分牌是通过等待)
+基本结构
+保留站
+每个保留一条已经流出并且等待到本功能部件执行的指令的相关信息。包括操作数、操作码以及各种元数据。
+故而,需要有以下字段:
+Op:操作
+Qj,Qk:操作数保留站号
+Vj,Vk:源操作数值
+load的Vk保存偏移量
+Busy
+A:存放立即数字段 or 有效地址,仅用于load和store缓冲器
+Qi:寄存器状态表
+存放把结果写入该寄存器的保留站ID
+公共数据总线CDB
+用于发送各个功能部件的计算结果。如果具有多个执行部件且采用多流出流水线,则需要采用多条CDB。
+load缓冲器和store缓冲器
+浮点寄存器FP
+指令队列
+FIFO
+运算部件
+浮点加法器、浮点乘法器
+寄存器换名实现
+当指令流出,如果操作数缺失,则将指令数据换名为保留站编号
+特点
+冲突检测与指令执行是分布的
+通过保留站和CDB实现
+计算结果通过CDB直接从产生它的保留站传送到所有需要它的功能部件,无需经过寄存器
+消除了WAW和WAR
+执行步骤
+3段流水
+流出
+如果操作要求的保留站空闲(结构冲突),则送到保留站r。如果操作数已就绪,填入;否则,填入产生该操作数的保留站ID(寄存器换名,消除WAW、WAR)。
+执行
+两个操作数就绪后,就可以用保留站对应功能部件执行
+写结果
+计算完毕后由CDB传送
+假设每个时钟周期流出两条,1整数型指令+1浮点型指令。
+整数型:load、store、分支
+浮点型:可能各种运算吧
+假设所有浮点指令都是加法,执行时间3个时钟周期,且图中整数总在浮点前
+没懂,难道单发射流水线就不会吗。。。
+后面不知道为什么写着写着开始英文了。。。算了,看起来都不重要。
+GPU中的线程是执行计算任务的最小单位,可以看作是一系列指令的执行者。每个线程都有自己的程序计数器(PC)、寄存器集和局部内存。这些线程以并行的方式执行相同的指令,但可以有不同的输入数据,从而在数据并行的模式下执行计算。
+下面两个标题反了额
+感觉能明白其划分一组组线程的意义了,就是方便管理,一个warp执行相同的指令代码,所以要求同时调度同时执行
+真没懂。。。。
+真没看懂
+TODO接下来有兴趣看吧
+idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/
这一段写得很好,非常易懂地概括了什么是“多线程把异步转化为同步”:把异步中的不同操作分解为一个个独立的同类型操作,然后只需实现这些相较简单的同类型操作,再异步地把它们调度起来就行。线程正是把复杂的异步工作流分解成了一组简单的同步工作流。
+如果一个模块在代码中引入了并发性,那么它所有的代码路径【调用链】都得是并发的。
+最后一句话很关键,“把线程安全性封装在共享对象内部”
+这个不同于上面的方法:将共享对象包装为线程安全的。它是要求了这些共享对象仅能在事件线程中运行,这样来保证线程安全性。
+**线程安全的核心就是对状态的访问和操作进行管理**,特别是对那些共享(shared)的、可变(mutable)的状态。关于本句话,其中几点将在下面一一细说:
+状态
+状态是指存储在状态变量里的数据,如成员变量、静态域等等等。对象的状态还可能包括其他依赖对象的域,如HashMap的状态包括Map.Entry的状态。
+共享和可变
+共享意味着变量可以由多个线程同时访问,可变意味着变量的值在生命周期可发生变化
+是否需要线程安全
+取决于它是否被多个线程访问。比如说,如果一个局部变量仅在某个函数体中同时只被一个线程访问,那么它就不需要线程安全,不需要同步机制。
+注意,线程安全不会违背不变性和后验条件,这句话在后面会用到。
+在此举例一个无状态线程:
+
|
无状态对象一定是线程安全的
+我们可以在无状态对象的基础上为它增加一个域:
+这是线程不安全的,因为++count包含了三个动作:读取—修改—写入
+mov reg,count |
它并不具有原子性。
+在并发编程中,这种由于时序原因产生错误的情况叫做“竞态条件”。
+竞态条件有两种常见的类型。两种竞态条件的本质其实都是“基于对象之前的状态来定义对象状态的转换”。对于读取-修改-写入,是先copy原值,然后对原值+1,再写回,这是基于对象之前的状态来定义对象状态的转换;对于先检查后执行,很显然就是判断原值然后再转换到下一个状态,这就不必说了。
+如上引例
+实例:懒加载,延迟初始化中的竞态条件
+public class LazyInitRace { |
这书里讲得云里雾里的,百度了一下:
+比如说书给例子,线程向共享对象读写数据,线程是操作对象A,共享对象是被操作对象B。则:
+竞态条件:在乎的是被线程操控的共享对象的结果是否正确
+数据竞争:在乎的是操作共享对象后,线程的结果是否正确。
+确实,书里对数据竞争强调的是一个读一个写,对竞态条件更像是两个同时写
+我们可以用一个线程安全类来解决前面的Count请求的需求:
+
|
上面说到,当对象内仅有一个状态时,可以通过使用线程安全类来保障原子性。但当对象里存在多个状态时,就必须用锁来进行线程同步,而非简单地用多个线程安全类。
+还是以上面的实例来解释。
+public class UnsafeCachingFactorizer implements Servlet{ |
这段论述非常精彩,昭示了两个道理:1.分析线程安全性的时候,可以从“不变性条件不被破坏”开始考虑,首先考虑不变性条件应该是什么。2.在不变性条件涉及的多个变量彼此不独立,因而这些变量需要同时同步更新,上面那个例子就是因为不变性约束条件中的两个不独立变量没有同时同步更新。
+确实,非常重要的一点就是在两个需要连续同时修改的变量之间有了并行的时间间隔,导致此期间并行的线程的不变性被破坏。
+同步代码块包含两部分,锁的引用和保护的代码段。关键字synchronized修饰的方法就是一段同步代码段,其锁对象为当前实例【非静态方法】或者是当前class的实例【静态方法】。
+++这个具体的“锁”是什么以前是真不知道。已知的是所有Object都有wait和什么什么notify方法。不过想想也确实。所有线程争抢着访问一个对象的某个同步方法段,这不正跟所有线程争抢着一个锁是差不多意思的吗?“锁”的定义其实是很宽泛的
+
java的内置锁并非无饥饿的。当线程B永远不释放锁,A会一直等待下去。
+我们可以用synchronized来解决上面的计数器问题,即直接给service方法设为synchronized。当然这种方法性能很糟糕,因为它极大降低了并发度。
+其中关于粒度的理解:
+不是“每一次调用获取一次锁,该锁属于该此调用”,而是“每个线程调用时获取一次锁,该锁属于该线程”
+public class Widget { |
比如上述代码,创建了一个LoggingWidget实例,然后调用该实例的dosmething方法,就会获取到该实例的锁。如果不允许重入,那么在做super.doSomething时,该实例的锁【注意,是同一个实例】已经被占用还未释放,因此产生死锁。有重入就可以避免此问题。
+但这很考验人的记性。一旦你在某个地方忘了同步了就会寄。
+上面那个直接对service方法进行synchronized的改善方法粒度太粗了,可以试试如下方法:
+
|
毕竟因数分解的时候无需同步保护,因为这时候参与运算的都是局部变量。
+上一章讲述了,线程安全的本质就是对共享和可变状态进行管理,以及介绍了用锁来保护状态。
+本章将引入同步除原子性外的另一特性——可见性,然后再介绍如何构建线程安全类,并且安全地发布和共享对象。
+关键词:可见性 Volatile 线程封闭 不可变对象
+public class Main { |
++关于此程序显示出的对于内存可见性的理解,可以看这篇文章:
+ ++ +
其实原因非常显而易见:主线程改了之后不会立刻把变量刷新到主存【可能默认是在ret时刷新,或者定时刷新,前者会导致相互等待的死锁,后者也会产生性能问题】,导致主线程的那个修改的flag变量对t1线程是**不可见**的,因此t1会继续循环等待。
+
注意,最低安全性不适用于非volatile类型的64位数据。
+要实现这种操作,我们可以设想一下关于内存可见性这一块内置锁的实现原理:lock时绑定指定变量,unlock时再刷新这个/些绑定变量的内存。
+所以说得有锁,并且锁还得是对的。
+看着看着有种always语句块的感觉了2333
+注意不放在寄存器里或者线程的私有栈里
++++
+ ++但这个有争议:
++ +
+
+- 在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。
+也就是说,
+如果线程B在+1前知道数据无效了,就会重新载入数据然后+1然后载入内存,结果正确;
+如果线程B在+1后才知道数据无效,虽然会重新载入数据,数据为A修改后的新数据,但是此时指令无法回退,因而只能继续执行下一条指令:写回内存,B写回内存的是A修改后的新数据,因而结果错误。
++ +
大致过程:
a:mov reg 1
b:mov reg 1
a:add reg 1
b:add reg 1
a:mov reg mem
然后b线程得到通知,重新载入数据:mov reg mem
但是指令无法回退:mov reg mem
因而结果是A修改后的值被写入了两遍。所以其实volatile仅确保单次读写的瞬时线程安全。
++ +
以下是别人的理解扩展:
+ + + +
下面给出一个volatile的典型用法:检查某个状态标记以判断是否退出循环。【也就是上文那个例子】
+volatile boolean asleep; |
这个“逸出作用域”的表述非常不错。
+当把一个对象传递给某个外部方法,就相当于发布了这个对象。
+外部方法:
“this escape”
+public class ThisEscape{ |
++java this 逸出_Java并发编程——this引用逸出(“this” Escape)
+并发编程实践中,this引用逃逸(“this”escape)是指对象还没有构造完成,它的this引用就被发布出去了。
+ThisEscape在构造函数中引入了一个内部类EventListener,而内部类会自动的持有其外部类(这里是ThisEscape)的this引用。source.registerListener会将内部类发布出去,从而ThisEscape.this引用也随着内部类被发布了出去。但此时ThisEscape对象还没有构造完成 —— id已被赋值为1,但name还没被赋值,仍然为null。这样一来,就有些线程持有不完整实例,不确定性太大了
+
也就是说,如果是单线程情况下,这样做是没问题的,毕竟最后都会构造完整。但多线程情况下,这俩有时间间隔,因此会产生问题,并且不能靠简单地把这句发布对象的语句放在构造函数最后一行。
+这段话非常值得注意
+所以说上面那个例子的正确代码:
+public class SafeListener { |
线程封闭一般有三种方法,这三种方法的规范性是逐级递增的。
+++这里,书写得非常地抽象。通过查阅资料可得解释得更通俗的:
+ +Example of ad hoc thread confinement in Java
+总之其实精华就这一句话:
+并且都是人为约束,并且一般可能会用volatile来控制单线程写这种情况下的同步。
++
// Don't modify this from any other thread than Thread X.
// So use it read-only for those other threads.
private volatile int someNumber;
也就是我们前面说的,局部变量只能在该线程内访问,除非逸出了,否则是非常安全的。
+因而需要格外注意逸出问题
+下面给出对基本类型和引用类型栈封闭的实例:
+public int loadTheArk(Collection<Animal> candidates){ |
+ ++
上面介绍了使用局部变量来实现线程封闭的方法,也就是栈封闭。它只要合理地控制在调用方法时不发生逸出,就可以实现线程安全。
+当有多个线程都需要同一类对象【比如Connection对象、ThreadID】,并且要求每个线程内的该对象是不一样的,并且该对象需要在多个方法中访问,栈封闭的方法就显得有些麻烦和不够优雅:需要在每个线程内都创建一个不同的对象实例,并且在调用方法的时候,都把该对象实例作为参数传进去。
+这时候就需要ThreadLocal类了。
+ThreadLocal类会给每个线程分配一个对象,并且仅需使用get方法,就能自动地把线程中的对象给弄出来。并且这些分配的对象对于各个线程来说都是隔离,相互不可见的,因此实现了线程封闭,具有安全性。
+以ThreadID为例:
+For example, the class below generates unique identifiers local to each thread. A thread’s id is assigned the first time it invokes ThreadId.get() and remains unchanged on subsequent calls.下面代码保证每个线程首次调用ThreadId.get方法后可以分配到一个不重ID,并且ID一旦确定,之后再调用get方法得到的ID是不会改变的。它这相当于维护了一个共有的计数器局部变量。
+import java.util.concurrent.atomic.AtomicInteger; |
对于每个想获取自身ThreadID的线程,在所有想用到ID的方法中,只需:
+void method(){ |
而不用:
+AtomicInteger myID = getID(); |
这样大大简化了实现。
+再比如:
+private static final String DB_URL = ""; |
关于这个的大概代码猜想:
+这样一来,在一个线程中使用toString,就仅需造一个buf【这个是ThreadLocal封闭】,而不用每次调用都造一个【这个是栈封闭】了
+private static final ThreadLocal<char[]> buf |
我们可以初步猜想,ThreadLocal大概是通过一个map实现的,里面存储着<Thread,value>这样的键值对,每次就能通过Thread来取出对应的value了。Java低版本确实是这么做的。但Java的高版本对此进行了优化。
+从本来的:ThreadLocalMap<Thread, value> ∈ ThreadLocal
+变成了: ThreadLocalMap<ThreadLocal, value> ∈ Thread
+并且其中的ThreadLocal这个key是以弱引用【WeakReference】的方式实现的。
++ ++这样的结构演进有什么好处
+
在旧版的ThreadLocal中,所有线程都将本地变量存在同一个ThreadLocalMap中,当并发量比较高的时候,ThreadLocalMap中的数据量会很大,而新版的ThreadLocalMap是属于线程的,也就是每个线程都操作属于自己的ThreadLocalMap,那么map中存储的变量就只有自己所存入的,数据量大大减少。还有一个好处,旧版的ThreadLocalMap属于ThreadLocal,当Thread实例被销毁时ThreadLocalMap里该线程的数据不会被同时销毁【这也许就带来了危险性】,而新版ThreadLocalMap属于线程,线程被销毁时,ThreadLocalMap也随之销毁。
+
++This class provides thread-local variables.线程局部变量,也就是我们说的线程封闭手法。
+These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. 每个线程都有它自己的、独立初始化的该变量的副本。
+ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).它一般用于私有静态字段,whose 状态和线程关系密切。
+Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the ThreadLocal instance is accessible.
+After a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).最后会被垃圾回收
+
public class ThreadLocal<T> { |
注意点:
+存在哈希冲突的话,大概是采用的线性探测方法。
+关于其remove方法:
+public void remove() { |
两篇文章都有解释
+++remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。
+实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。
+所以如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个 ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。
+ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。
+
————————————————
版权声明:本文为CSDN博主「倔强的不服」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u010445301/article/details/111322569
++ThreadLocal内存泄漏问题的解析。
+
前面我们说到它虽然线程安全,但是它存在一个问题那就是内存泄漏。首先我们要明白为什么会内存泄漏,前面也说了ThreaLocal是一个弱引用,什么是弱引用就是当它为null时候,就会被垃圾回收机制给带走,重点就是,如果我们的ThreadLocal突然为null,然后就被回收了,但此时我们的ThreadLocalMap它的生命周期是和Thread相同的,简单理解就是,裤子没了,兜还在,兜里面还有我们的数据,这就造成了内存泄漏。
+如何解决那:我们必须在使用完ThreadLocal后,执行remove()方法,避免内存溢出。
+
————————————————
版权声明:本文为CSDN博主「某刘姓男子i的码农客栈」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_20783497/article/details/107980858
满足同步需求的另一种方案就是使用不可变对象。
+这个思路非常地简单粗暴:什么东西影响了,就直接让它消失。非常有意思2333
+如果某个对象在创建后不能被修改,那么它就叫不可变对象。线程安全性是不可变对象的固有属性之一。
+比如说final域只能在声明的成员域或者构造函数中初始化,两者本质上都是在构造函数中初始化的。
+并且不可变对象也更加安全。
+不可变性不等于将对象中的所有域都设置为final域,因为final类型的域可以是对可变对象的引用。【这就类似C语言中const指针】当且仅当满足下列条件,对象才是不可变的:
+++对于这里注释提到的String类,它讲得有些让人迷惑。因而我查阅资料得到解说如下:
+ ++ +
class String{
//默认值是0
int hash;
public int hashCode() {
//将成员变量hash缓存到局部变量
int h = hash;
//这里使用的是局部变量,因此没有多线程修改的风险
if (h == 0 && value.length > 0) {
char val[] = value;
//求hashcode过程使用局部h变量防止产生静态条件
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
//把求出的hashcode缓存到局部变量,原子操作
//这里不需要考虑线程可见性的问题,
//如果其它线程未能及时看到最新修改,重新计算hash值代价也不大
hash = h;
}
return h;
}
}再回去看书中注释的描述:
++ +
这个的意思就是说,对每个线程来说,同一个字符串hashcode值都是一样的【每次计算都得到相同的结果】,所以就不会产生多个线程计算出不同值的情况,导致不同步的发生。
++ +
意思是说之所以hashcode值一样,是因为这个hashcode计算是基于不可变对象的:
++ +
private final char value[];并且重复计算性能代价可能远没有加锁的消耗来得大,因而这里仅使用了栈封闭来保证一定程度上的线程同步。
+
public final class ThreeStooge { |
也就是说,实现的核心是保证可变对象不变即可。
+final不仅保证了引用对象的不可变,还保证了不可变对象初始化过程中的线程安全性。
+所以说还是尽量多用final。
+也就是这个图片中所说的:
+使用volatile变量来发布不可变对象,不仅可以更新保存在不可变对象中的程序状态,还可以为一组操作提供弱原子性。
+前面的代码为:
+public class UnsafeCachingFactorizer implements Servlet{ |
如今,利用volatile和不可变类的相互配合,我们修改如下:
+public class VolatileCachedFactorizer implements Servlet{ |
每当需要对一组相关数据以原子方式执行某个操作时,就可以考虑创建一个不可变的类来包含这些数据,其本质就是利用不可变性消除了访问和更新多个变量的竞态条件。
+因为依然满足该程序的不变性原理:factor数组中各个数字的乘积=lastNumber,也就是说容器对象的两个值都是正确对应的,因而容器对象处于一致的状态。又或者是因为volatile及时刷新,因此确保了各个线程的内存可见性。
+现在,我们要来讲讲如何安全地对对象进行发布。
+public Holder holder; |
Holder类本身是没有问题的,这段代码出问题的原因是holder没有被正确地发布。
+关于holder为什么没有被正确地发布:
+由参考文章1:
+可知,new一个对象并非原子操作,并且很有可能先得到内存引用才初始化对象。
+因而,在上面那段不安全代码的语境下可分析:
+首先明确,引用,和引用的对象的状态,这两个是两个需要独立考虑的方面。前者是一个指针值,后者是指针所指的数据。下面的点1仅考虑引用的更新,点2考虑了引用对象的状态更新。
+\1. 发布对象的那个线程给holder初始化之后,holder这个引用没有及时刷新到内存,因而对其他线程不可见,其他线程读到的holder引用是旧的。
+\2. 又或者,发布了holder还没初始化完毕的时候,别的进程读取到未完成初始化的holder这个引用,但这个引用指向的状态却是旧的,因为它还没完成初始化,其状态值为旧值或者默认值。【发生了上面new一个对象的指令重排】
+\3. 如果在assert方法中两次读取n发生了上面第二条,就可能会导致前后的n不唯一,抛出异常。
+由书中表述,如果将Holder转化为不可变类,那么该发布是安全的。
+至于为什么,可见下个标题。
+++此处插入思考:是否可以将
+ +public Holder holder
修改为public final Holder holder
,或者volatile修饰,来解决上述问题呢?通过看该文章得知:
++ +
volatile和final都会禁止字段引用的对象在构造对象过程中发生指令重排,别的线程得到引用的时候构造已经完成,而不会先得到引用再完成构造,并且两个标志都可以保证可见性。
+不过继续读下去,书中给出了答案:我说的这个方法也是可行的。
++ +
我的疑问就是第二点和第三点。
+
分别解说
+详见上面那个不正确案例最后的思考
+这个区域除了是通过程序构造的,也可以是使用Java自带的线程安全类库。
+安全发布可以保证发布时的线程安全。所以说你如果承诺发布后可以一直保证不可变,那就一直都是线程安全的。
+也就是说上面都是在讲怎么让一个对象的共享变得安全,下面我们讲怎么依据设计模式,让一个类更容易成为线程安全的
+要保证不变性条件始终成立,确保后验条件符合预期。
+讲了什么是不变性条件和后验条件:
+无效的状态转换只能出现在原子序列中
+也就是说先验条件和状态域相关。
+666666
+所以需要上一章学的安全发布。
+//通过封闭机制保证线程安全 |
阅读源码可知:
+static class SynchronizedCollection<E> implements Collection<E>, Serializable { |
就是把原来的collection给实例封闭了,之后的访问都用了同步锁。
+直白点来说,就是把所有要访问自己状态的地方/方法通通synchronized。
+//监视器模式 |
这样虽然简单,但缺点就是很粗暴:同步的粒度太粗了。
+也跟内置锁道理差不多
+也就是说私有锁可以让外面的世界也参与到同步中来,但内置锁不大行。
+public class MutablePoint { |
//基于监视器模式 |
意思就是保证一个类里面仅有一个状态,只要该状态是线程安全的,那么该类也就是线程安全的。
+//线程安全 |
//将线程安全委托给ConcurrentMap |
public Map<String,Point> getLocations(){ |
也就是说这些对象彼此不会构成不变性条件。
+//将线程安全性委托给多个状态变量 |
而且键盘监听和鼠标监听彼此独立。
+public class NumberRange { |
根本原因就是因为不独立。
+package sit; |
//跟上面那个委托没什么差,区别只在于上面的那个SafePoint类,既是线程安全的,又是可修改的 |
这仅仅是一个委托发布的实例。
+比如说想给vector添加一个“put-if-absent”
+可以用子类扩展法,也可以直接加源代码。后者有时候源代码不可访问,前者的父类很多域可能不对子类开发,并且非常脆弱。因而下面介绍几种比较好的机制。
+
|
我曹,66666666
+也就是说,这里加的是ListHelper的锁,只能让别的线程不能通过putIfAbsent方法同时修改list,但别的线程完全可以直接获取list再修改。
+客户端指的是我们的ListHelper。我们正是不知道list这个对象使用的是哪一个锁才发愣的。
+所以我们使用ArrayList自身的锁,也就是list自己的内置,来加锁。
+//使用客户端加锁实现 |
它非常依赖于其他类的客户端加锁机制。
+确实,毕竟你锁被外界拿去用了。
+public class ImprovedList<T> implements List<T> { |
是的,跟synchronizedList非常像
+这也就是用的java的监视器模式了
+保证独立即可委托,从而构建一个线程安全类
+差不多都是使用的监视器模式。
+同步容器类:Vector、Hashtable、Collections.synchronizedXxx
+也就是需要避免两个原子操作之间的非线程安全的时间间隔。
+public static Object getLast(Vector list){ |
它这段话说得非常好。由于Vector这个类本身是线程安全的,因而它可以保证外部任何操作都不会导致该对象因为并发而被破坏。但是,我们用不加锁的复合操作虽然不会破坏Vector,但可能导致不能出现我们想要的结果。
+所以我们必须用锁机制来对此复合操作进行保护:
+public static Object getLast(Vector list){ |
除此之外,迭代也是一种经典的复合操作。我们可以通过下面这种粗粒度加锁来避免:
+public static void travel(Vector list){ |
所以才会引入fail-fast机制。
+++foreach语法糖内部是通过Iterator来实现的。
+ ++
public void testIterableForEach() {
List<String> list = new ArrayList<>();
for (String str : list) {
System.out.println(str);
}
}
//反编译后:
public void testIterableForEach() {
List<String> list = new ArrayList<>();
Iterator i = list.iterator();
while(i.hashNext()){
String str = (String)i.next();
System.out.println(str);
}
}
可见同步容器类还是有很多局限性的。
+有时候,迭代会隐藏起来。要一个个揪出需要加锁的地方是非常麻烦的。
+//隐藏在字符串连接中的迭代操作 |
同步容器类的加锁太粗粒度了,导致并发性弱。因而引入并发容器类来解决问题。
+ConcurrentHashMap —— HashMap
+CopyOnWriteArrayList —— List
+BlockingQueue
+ConcurrentSkipListMap —— TreeMap
+ConcurrentSkipListSet —— TreeSet
+使用分段锁来细粒度加锁。
+++关于ConcurrentHashMap的分段锁:ConcurrentHashMap
+JDK1.7中,ConcurrentHashMap 类所采用的正是分段锁的思想,将 HashMap 进行切割,把 HashMap 中的哈希数组切分成小数组,每个小数组有 n 个 HashEntry 组成,其中小数组继承自
+ReentrantLock(可重入锁)
,这个小数组名叫Segment
。JDK1.8 中 ConcurrentHashMap 类取消了 Segment 分段锁,采用
+CAS
+synchronized
来保证并发安全,数据结构跟 jdk1.8 中 HashMap 结构类似,都是数组 + 链表(当链表长度大于 8 时,链表结构转为红黑二叉树)结构。ConcurrentHashMap 中 synchronized 只锁定当前链表或红黑二叉树的首节点,只要节点 hash 不冲突,就不会产生并发,相比 JDK1.7 的 ConcurrentHashMap 效率又提升了 N 倍!
+
++关于ConcurrentHashMap的弱一致性:ConcurrentHashMap的弱一致性
+get方法是弱一致的,是什么含义?可能你期望往ConcurrentHashMap底层数据结构中加入一个元素后,立马能对get可见,但ConcurrentHashMap并不能如你所愿。换句话说,put操作将一个元素加入到底层数据结构后,get可能在某段时间内还看不到这个元素,若不考虑内存模型,单从代码逻辑上来看,却是应该可以看得到的。
+
精确值—>估计值
+++关于AQS框架:重大发现,AQS加锁机制竟然跟Synchronized有惊人的相似
+在并发多线程的情况下,为了保证数据安全性,一般我们会对数据进行加锁,通常使用Synchronized或者ReentrantLock同步锁。Synchronized是基于JVM实现,而ReentrantLock是基于Java代码层面实现的,底层是继承的AQS。
+AQS全称**
+AbstractQueuedSynchronizer
**,即抽象队列同步器,是一种用来构建锁和同步器的框架。我们常见的并发锁ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier都是基于AQS实现的,所以说不懂AQS实现原理的,就不能说了解Java锁。
+
++并发容器类不能实现独占访问:
+类似ConcurrentHashMap的并发容器不能采用客户端加锁机制,因为并发容器没有采用synchronized内置锁而大多基于AQS框架(不是独占式的锁),所以使用客户端加锁机制来扩展并发容器的方法是不能实现的。
+所以说不能客户端加锁不是不提倡,而是真的不行【】
+
所以最好还是用并发容器类替代同步容器类。
+客户端加锁不能使用,就只能用它提供的东西了。
+Copy-On-Write
意为“写入时复制”,仅当要修改的时候,才会重新创建一次副本,实现可变性。犹记得第一次接触到这个思想是在操作系统的fork()创建子进程的原理那个地方,那可真是有些惊为天人23333
也就是说,COWAL内部维护的base数组是事实不可变的,因而访问它的时候不需要同步。但是,我们事实上需要一个可变的并发容器,那该怎么办呢?解决方法就是每次要修改的时候,直接把base数组换成一个新的数组,就像之前某个例子一样,这样就能实现可变性了。
+与此同时,这样的方法也能保证多线程访问时的内存可见性。
+由COWAL的底层代码:
+//base数组,volatile保证引用一变就可以刷新 |
可知,它保证可见性,是直接修改引用的,并且注意,对原数组的拷贝是浅拷贝的。这样一来,就既不会改变原数组的东西,也能保证可见性的更新迅速了。我的评价是牛逼爆了。
+简直就是为了生产者消费者而生的
+这两段话说得非常本质,需要有个缓冲队列本质上就是因为处理数据速率的不同,生产者消费者也起到了解耦作用
+所以说用有界队列还是更好
+BlockingQueue有多种实现。
+ArrayBlockingQueue和LinkedBlockingQueue是FIFO队列,PriorityBlockingQueue是优先队列,最后还有一个特殊的SynhronousQueue。
+//生产者 |
//启动生产者-消费者程序 |
安全发布
+6666666
+之所以叫“串行”,想必是因为这个过程:发布-转接-放弃访问权是一个串行过程。
+总而言之,串行线程封闭的具体做法就是,一个线程将一个安全发布的对象的所有权完全转移给另一个线程,保证之后自己不会再使用。这样一来,该对象就相当于被另一个线程封闭了。而如何保证“自己以后不再使用”呢?最简单的方法就是安全发布完这个东西后直接把这个东西给踢出去。
+阻塞队列是自动会把这个东西安全发布然后就踢出去的,所以说阻塞队列简化了这个工作。
+当某方法抛出InterruptedException时,表明该方法为阻塞方法,也即这个方法会在执行过程中由于各种原因而被阻塞。如果这个方法被中断,它将会努力提前结束阻塞状态。
+++此处关于为什么如果代码是Runnable的一部分就不能抛出异常:
+是因为java的异常继承体系。
+在重写的run方法中,我们只能够进行异常的捕获而不能够抛出异常,原因是因为在父类Runnable接口中,run方法没有抛出异常,则实现Runnable的子类就无法抛出异常
+所以实际上是语法层面上不允许。
+
实现同步的方法:使用同步容器类/并发容器类、使用锁、使用同步工具类
+//使用CountDownLatch来进行计时测试 |
是的,这样测试出来的时间应该更加平均,性能更加准确。
++ ++关于Future:
++ +
其中get方法是阻塞的。
+获取异步任务执行完后的结果。
+关于FutureTask:
+FutureTask既包含了Future的语义,又包含了Runnable的语义。
+它其实内部封装了一个Runnable Task。调用FutureTask的run,其实本质上就是调用Task的run,只不过要多一些检查和存储结果之类的手续。
+所以说它其实就是通过内部封装一个线程,然后就能获取这个线程运行的状态和运行的结果等等等,这样来实现Future语义的。
+
++关于Callable
+Runnable里面的run方法是不能传参,也没有返回值的。Callable相当于有返回值的Runnable,也即书中说的“有生成结果的Runnable”。
+
public class Preloader { |
也就是调用start线程启动后,可以就去做别的事情,回来就可以拿到结果了,通过这样实现异步调用。
+这里其实是在讲异常的事了。可以给我们一个启发式思路:
+Callable抛出的Exception这种抽象的异常集合该如何分解处理:首先分解出受检查的异常【也就是说我们调用该方法就知道该方法可能会抛出的异常】,然后针对其他未检查异常,再进行处理。此例中是把这些未检查异常分成了Error和RuntimeException。
+++注意
++
这种情况下,其实信号量跟BlockingQueue语义十分近似:
+信号量还可以用来将非阻塞容器包装为有界阻塞容器。
+//使用信号量为容器设置边界,有界+阻塞 |
++注:try语句块正常return后,finally语句依然会执行:
++
public class Main{
public static void main(String[] args) {
System.out.println(haha());
}
public static int haha(){
int number = 10;
try{
System.out.println("main!");
return number++;
}finally {
System.out.println("come!"+number);
}
}
}
/*输出结果
main!
come!11
10*/
闭锁是某个事件发生后所有线程才能继续执行;栅栏是所有线程都在同样位置等待才能继续执行。
+一个线程寄了,其他所有等待线程都会死。
+栅栏我觉得一个很重要的点就是保证并发安全。
+常常出现那种需要等待所有线程都完成某一步操作才能进行下一步操作的情况,所以栅栏不得不说非常实用。
+public class CellularAutomata { |
++注:
+的目的
++
这个“写满”大概对应着栅栏思想里的“全部到达”
+public interface Computable <A,V>{ |
//感受一下cache也是Computable的这个多态运用的巧妙性 |
保证了线程安全,但是可伸缩性极差,而且很有可能变成普通的串行排队计算。
+public class Memoizer2 <A,V> implements Computable<A,V>{ |
确实加锁的粒度小了【没有把高耗时的compute过程锁上】,但会带来某个值重复计算的问题。
+而且这不仅仅是性能损耗问题,还有可能会变成安全隐患。
+public class Memoizer3<A,V> implements Computable<A,V> { |
这个“判断是否开始”与“判断是否完成”的表述非常有意思。此时cache变成了arg和一个异步任务得到的未来结果的映射,而非arg和结果的映射。从此处也可好好理解感受一下“Future”的语义(一个异步执行的结果)。
+只是它依然没有解决上面所说的问题,还是可能会有两个线程计算同一个值,虽然概率小得多,主要原因是因为它使用了非原子的“先检查后执行”。
+因而,我们可以通过map提供的putifabsent同步方法来解决这个问题。
+public class Memoizer<A,V> implements Computable<A,V> { |
虽然这个最终版本看起来很完美,但实际上,它还会带来其他的性能问题。
+public class Factorizer implements Servlet{ |
也就是说,一个请求视为一个任务。这样做是非常reasonable的,因为每个请求间都是独立的。
+调度任务最简单粗暴的就是直接让任务串行执行。
+//串行的web服务器 |
这里有一个点非常棒:网络也是IO,也会造成阻塞。
+public class SingleThreadWebServer { |
但是其实这种方式是不好的,因为它无限制地创建线程,这听起来就很容易寄,要知道,高并发的服务器可能会一次解决几千万个请求【当然不知道有没有那么多hhh】,每个都创建一个线程的话,很容易爆内存,而且还会有很大的性能开销。
+而且这样的话,要是高并发情况下,服务器会马上崩溃,做不到我们之前说的自我调节功能。
+所以,我们应该为系统能创建的线程数做一个限制。如此,便引出了Executor框架。
+/* |
public class TaskExecutionWebServer { |
这里感觉绕来绕去的,execute到底是会创建一个线程,还是不会创建一个线程?到底是在调用时就创建线程执行任务,还是会在将来的某一个时刻调度执行任务?它这里说得云里雾里的,我来锐评一下我的看法。
+首先,我认为,它给我们的ExecutorService类的execute应该都仅仅是提交任务,放进任务队列,之所以什么时候执行得看调度情况。【Form java ThreadPoolExecutor.execute: Executes the given task sometime in the future. 】
+而下面那两个类应该都是对execute进行了简单的重写,因而此处execute跟java包里的ExecuteService没有任何关系,调用execute仅相当于调用一个普通的方法。
++ ++
Executor作为一个接口,其核心思想便是“解耦”。
+如果没有Executor的话,我们要创建并运行一个任务,一般都得这样用:new Thread(....).start()
,或者是比如说串行的new Runnable(...).run()
。也就是我们将任务的创建和任务的执行都混在一起了。而假定说,如果以后要改变该线程池的执行方式,比如说从单任务单线程的并行改成全部任务都串行或者反之,那么就需要每个地方都改掉。但如果使用Executor框架将任务创建和任务具体执行解耦开来,那么我们就仅需修改任务具体执行了。
JUC(java.util.concurrent
)其实就只是分为三个部分。
ExecutorService
+ThreadPoolExecutor继承了该接口。
+是Executor接口的加强版,包含了更多方法,具体为:
+① 自身生命周期的管理 shutdown、isshutdown等等
+② 对异步任务的支持 返回Future的submit方法
+③ 对批处理任务的支持 invokeall
+内部原理
+当空闲的线程足够多,直接执行;当线程不够多,进入阻塞队列;当阻塞队列满,使用拒绝策略。
+内部的线程池分为救急线程和核心线程。核心线程一直存在,当阻塞队列和核心线程都不够用,就会新开几个救急线程。
+说得非常全面
+66666
+我们结束executor,可以采取或温和或粗暴的方法:可以让它不接受新的,慢慢执行完全部再结束;也可以让它直接全部结束,管它有没有执行完或者有没有还没被执行,就跟断电一样。
+++此处疑问:不应该先shutdown再awaitTermination吗?我百度了,也都是说先shutdown。毕竟awaitTermination方法是阻塞的。
+
//支持关闭操作的Web服务器 |
public class OutOfTime { |
所以并发编程最难的其实还是建模,如何从串行中挖掘出并行性。
+这个把文字的render和图片的render都归结进图像缓存的统一化思想很有意思。
+//串行地渲染页面元素 |
显而易见,图像的IO需要耗费大量时间,这段时间内CPU都处于空闲状态,可以说利用率非常低下。
+意思就是Callable比Runnable有时候更灵活,因为Callable可以抛出异常,也可以有返回值。
+这个Future的说法很棒,只能说比起前面那个含糊的“表示一个异步执行的结果”,这个“任务的生命周期”方法更加醍醐灌顶。
+public interface Future<V> { |
|
其中,get的方法取决于任务的状态。
+可以利用返回的Future实例来对任务线程进行管理。
+将要求分解为两个任务:渲染文本和渲染图像。
+//使用Future等待图像下载 |
所以难点还是分解同构任务。
+CompletionService的思想其实和这个差不多。它主要就是多包装一层,数据结构的管理不用你写,更加方便。
+也就是说ExecutorCompletionService将CompletionService的计算部分交给了传进来的线程池Executor,然后自己管理一个阻塞队列,类似生产者-消费者模式,把线程池里出来的结果放进去。
+public class Renderer { |
我这里写了一个自己用list来保存Future结果的。不知道为什么这个不行,有待说明。
+public class MyRenderer { |
Page renderPageWithAd() throws InterruptedException{ |
/** |
也就是说,跟前面的CompletionService的优化目的是一致的,都是为了方便管理这一组future,这也跟我上面写的那个list管理版本是一样的。只不过区别在于,CompletionService还可以共用任务池,因而功能更强。invokeAll用法更简便。
+private class QuoteTask implements Callable<TravelQuote>{ |
中断是个重要概念,也算是老朋友了
+Java并没有提供取消某个线程的安全抢占方法,仅有约定俗成的协作机制。
+比如说,可以设置一个volatile类型的取消标志,并且让线程定期查看该标志。【这是volatile的经典用途】
+public class PrimGenerator implements Runnable{ |
使用实例:
+PrimGenerator generator = new PrimGenerator(); |
比如下面程序:
+public class BrokenPrimeProducer extends Thread{ |
所以解决方法其实很简单,只要让阻塞状态下我们还能知道要取消任务就行。这靠我们在表层写代码是做不到的,需要用到Java提供的另一种协作机制:线程中断。
+所以说中断其实就是为取消而量身定做的。
+public class Thread{ |
所以作为上层开发者,我们仅需捕获中断异常即可。
+也就是说,打断阻塞状态下的线程会清空中断状态,打断正常状态的线程会保持中断状态。而正常状态的线程如果不对中断状态处理,就会一直保持中断状态然后继续运行,也就是屏蔽中断状态。
+具体检查方法还是定期看标记。在看到标记后可以做善后工作再决定停不停。
+程序清单5-10:
+恢复中断状态的示例:
+捕获睡眠时的中断异常,然后重新设置打断标志为true,进入下一次循环时再对标记进行处理。
+//注意此处继承自Thread |
测试主函数部分如下:
+PrimeProducer generator = new PrimeProducer(new ArrayBlockingQueue<>(10)); |
此处编写主函数运行时,不小心产生了一个错误:Why does the ThreadpoolExecutor code never stop running?
+public static void main(String[] args) { |
这段代码跑起来的最终结果就是进程永远无法终止。至于为什么:
+PrimeProducer类继承自Thread,而execute的参数是一个Runnable。也就是说,Executor会把传进来的这个Thread当成一个Runnable,然后再把它包装成一个新的Thread。所以你的generator里的cancel方法:
+public void cancel(){interrupt();} |
调用的就不是本线程的中断方法,而是一个全新的毫无关系的线程的中断方法了。
+所以其实应该这么写:
+public static void main(String[] args) { |
但我还是有个奇思妙想。可不可以沿用一开始那个错误的主方法版本,然后修改PrimeProducer类为:
+//此处修改为Runnable |
结果还是跑不起来,不知道为什么,有待解答。
+意思就是,单个的任务是非线程所有者,因为它们是被分配到线程池所有的线程执行的。所以它们不能直接对中断进行处理,需要把中断异常抛给那个目前还不知道是谁的所有者线程,让调用者决定自己该怎么做。
+以下的地方一个字也看不懂,写自己的思考也没什么意义。就附上正确代码模板吧。
+//通过future定时取消任务 |
先放个通关记录~
+++特别鸣谢:
+某不愿透露姓名的友人hhj
+ + +
++During the semester, you will build a disk-oriented storage manager for the BusTub DBMS.
+注:DBMS(Database Management System),比如说Oracle数据库
+The first programming project is to implement a buffer pool in your storage manager.The buffer pool is responsible for moving physical pages back and forth from main memory to disk.也就是负责物理页从磁盘的读写
+It allows a DBMS to support databases that are larger than the amount of memory available to the system. 是的,这其实就跟内存换入换出差不多。我们现在是不是要在用户态实现这个功能?我记得xv6似乎是没有这个机制的。有点小期待呀。不过这部分感觉说不定和xv6的磁盘管理(使用双向链表管理buffer cache),及其的改进版本(lab:locking 使用哈希+双向链表)比较类似。
+++注:xv6确实没有内存换入换出机制,其是固定大小的内存空间。但xv6的文件系统有采用LRU算法的buffer cache(怪不得有什么数据库型的文件系统,这两个确实有点像)。
+The buffer pool’s operations are transparent to other parts in the system. For example, the system asks the buffer pool for a page using its unique identifier (
+page_id_t
) and it does not know whether that page is already in memory or whether the system has to retrieve it from disk.Your implementation will need to be thread-safe.
+
由于这几天时间比较零散+事情比较多,因此完成的总时间数不一定值得参考:26h(乐)
+本次实验要说简单也还算简单。大概就是实现一个database与磁盘交换页的buffer pool,机制类似于内存换入换出。而实现这个buffer pool,首先得实现换入换出算法,也即task1的LRU-K算法。再然后就是在我们的LRU-K算法的基础上,实现真正的buffer pool(真正指:真正地存储以及读写磁盘、向外暴露接口),也即BufferPoolManager
。最后,我们会实现类似于lock_guard
这样结构的PageGuard
,用于自动释放页引用和读写锁。最后的最后,我们会对实现的buffer pool进行性能优化,优化方向包括细粒度化锁以实现并行IO、针对特定应用场景调整LRU-K策略等。
这四者都是相互联系相互递进的,我认为每一个task都设计得非常不错,写完了之后对它所涉及的知识点都有了更深刻的理解。我认为其中最优美的一点就是LRU-K算法与buffer pool的解耦,这个设计让我十分地赞叹。
+最后,再对我的完成情况进行一个评价。本次实验确实内容不是很难【除了性能调优部分,这个我是真不懂QAQ】,毕竟它指导书以及代码注释都给了详细的步骤参考,我之所以做了那么久一是因为我有不好的习惯,就是没认真看指导书和提示就开始按着自己的理解写,然后写完就直接开始debug开始交了;二是因为这几天学业的破事太多、竞赛也逐步开始了,因而战线拉得太长,总耗时就太多了。
+因而,吸取经验,我之后coding完了之后,再照着指导书仔仔细细地过一遍自己的代码。同时,15445这个实验我也决定先暂时搁置,毕竟接下来这两个月应该会在竞赛和学业两头转,实在不能抽出很大段时间继续写了。
+就酱。
+++This component is responsible for tracking page usage in the buffer pool.
+The LRU-K algorithm evicts a frame whose backward k-distance is maximum of all frames in the replacer. LRU-K 算法驱逐一个帧,其backward k-distance是替换器中所有帧的最大值。
+Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access.
+backward k-distance
=现在的时间戳 - 之前第k次访问时的时间戳A frame with fewer than k historical accesses is given +inf as its backward k-distance. 不足k次访问的帧的backward k-distance应该设置为inf(对应上图左边那个访问记录队列吧)
+**When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**如果有多个inf的结点,按照LRU规则淘汰(也即上图左边那个历史记录队列采取LRU规则)
+The maximum size for the
+LRUKReplacer
is the same as the size of the buffer pool since it contains placeholders for all of the frames in theBufferPoolManager
. However, at any given moment, not all the frames in the replacer are considered to be evictable. The size ofLRUKReplacer
is represented by the number of evictable frames. TheLRUKReplacer
is initialized to have no frames in it. Then, only when a frame is marked as evictable, replacer’s size will increase. size为可驱逐的frame数而非所有frame数。
本次实验要我们实现的是一个LRU-K算法的页面置换器。
+LRU-K算法是对LRU算法和LFU算法的折中优化,平衡了LFU和LRU的性能和开销的同时,也解决了缓存污染问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。具体来说,它维护了一个backward k-distance
,其计算方法:
backward k-distance
= current_timestamp_
- 倒数第k次访问的时间戳 backward k-distance
= +inf页面驱逐规则:
+驱逐 backward k-distance
最大的页。
也即情况2总是优先会比情况1被驱逐;每次优先驱逐previous k次访问最早的页面。
+当有多个页值为+inf,则采取FIFO规则进行驱逐。
+故而,在具体实现中,为了便于管理,我将此拆分为两个队列:
++ +++
+
+- +
数据第一次被访问,加入到访问历史列表;
+- +
如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;
+- +
当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;
+- +
缓存数据队列中被再次访问后,重新排序;
+- +
需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。
+
每个页面结构持有一个时间戳队列即可:
+task1的内容就是实现对一堆frame_id
的LRU-K算法管理,挺简单的(也可能是测试用例少我错误没排查出来2333)
我并没有用默认给的模板的unorder_map
,也没有用默认给的模板思路(但原理以及最终效果是差不多的,就是没用它的方法),而是选择类似像上面这张图一样,分成两个队列实现,一个队列visit_record_
存储那些访问次数<k的数据,另一个队列cache_data_
存储那些访问次数>=k的顺序,每次优先淘汰visit_record_
中的数据,两个队列都采用LRU的方式管理。与此同时,我觉得LRU管理时间戳只用记录最新访问的就行,所以将历史访问时间戳队列改成了只有一个变量。
++参考:
+FIFO和LRU这里面的实例非常直观地说明了两种算法的差异,可以跟着手推感受一下
+pro1这个用的是我上面的那个想法,是错的。但是评论很值得参考:
++
pro1这个评论的“偷测试用例”xswl,虽然这次没用,但以后说不定能用上:
++
……简单个屁!!
+算法上,上面错误的算法确实很简单;而正确的算法也确实很简单。那么难的是什么呢?我觉得难的还是搞清楚它要我们实现的究竟是上面东西。
+结合指导书这段话:
+++The LRU-K algorithm evicts a frame whose backward k-distance is maximum of all frames in the replacer. 每次驱逐
+backward k-distance
最大的那么
+backward k-distance
是什么?Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access.
+backward k-distance
=current_timestamp_
- 倒数第k次访问的时间戳A frame with fewer than k historical accesses is given +inf as its backward k-distance. 没有达到k次访问的,
+backward k-distance
为+inf。也就是说,每次优先从历史访问队列清除元素。【**When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**】当历史访问队列有多个元素,就驱逐英文描述那样的frame。
+
我们可以发现,它这个对于两个队列的LRU,并非我们原来算法那样,对于每个frame,针对其最新的访问时间戳,也即history_.back()
,进行LRU淘汰;而是,针对其倒数第k新的访问记录,也即history_.front() && history_.size()<=k
,进行LRU淘汰。
其中,由于历史访问队列的记录少于k个,因而其事实上从k-distance算法退化为了FIFO算法。【感受一下这一点的优美:FIFO实际上是k-distance的特例】
+我们上面的算法比较的是history_.back()
,所以可以省略时间戳队列为一个变量,然后将两个队列使用FILO的形式组织起来。正确算法就不能这么简单了,要按front排序的话,实现开销可能更大,所以下面就采用了map形式来实现logn的查找。
这里一个点我其实还是很疑惑的,完全想不通。
+就是,对缓存队列实现k-distance算法没毛病,这段话已经写得很清楚了。
+++Backward k-distance is computed as the difference in time between current timestamp and the timestamp of kth previous access.
+backward k-distance
=current_timestamp_
- 倒数第k次访问的时间戳
但是,为什么历史访问队列要用FIFO呢?是我英语不好吗,这段话不是实现纯正LRU的意思吗:
+++【**When multiple frames have +inf backward k-distance, the replacer evicts the frame with the earliest overall timestamp (i.e., the frame whose least-recent recorded access is the overall least recent access, overall, out of all frames).**】当历史访问队列有多个元素,就驱逐英文描述那样的frame。
+
我翻译一下:
+当多个frame有+inf这个 backward k-distance
的时候,replacer需要驱逐拥有全部(overall)frame中最早的timestamp的frame。(也就是说,frame,它的最近访问记录是所有frame里面最早的)
这样确实看起来就是要用LRU。
+但其实,是我英语不好。咨询了场外热心人士hhj之后,我才修订出了如下版本:
+当多个frame有+inf这个 backward k-distance
的时候,replacer需要驱逐拥有全部(overall)frame中最早的timestamp的frame。(也就是说选择一个frame,这个frame的最不近的访问记录,是所有frame中最近最少访问的)【也即这个frame的history的front是所有frame中最早的,也即使用FIFO算法】
可见,正确解法确实是没问题的,就是理解上很困难。要是可以配个实例就好了QAQ
+所以说,所谓LRU(Least Recently Used)的直译还是最不近使用,也即最近最少使用。里面这个least不是用来修饰recent表示recent程度深的,相反它表示的是recent的程度浅。英语不好的惨痛教训啊。
+最后一下子交了这么多次才过。绷不住了。
+++The
+BufferPoolManager
is responsible for fetching database pages from theDiskManager
and storing them in memory.从DiskManager中取出页,然后存入内存。也就是说,我们的Buffer Pool是磁盘到内存的映射,我们在Task1实现了内存部分的管理数据结构?
+The
+BufferPoolManager
can also write dirty pages out to disk when it is either explicitly instructed to do so or when it needs to evict a page to make space for a new page.也要负责dirty页的写回You will also not need to implement the code that actually reads and writes data to disk (this is called the
+DiskManager
in our implementation). We will provide that functionality.DiskManager
已给出All in-memory pages in the system are represented by
+Page
objects. EachPage
object contains a block of memory that theDiskManager
will use as a location to copy the contents of a physical page that it reads from disk.Page
是可复用的内存页容器The
+Page
object’s identifer (page_id
) keeps track of what physical page it contains; if aPage
object does not contain a physical page, then itspage_id
must be set toINVALID_PAGE_ID
.也就是说,
+page_id
表示的是实际的物理页号;frame_id
表示的是你的Page容器的序号,同时也是LRU的对象。你需要一个类似<fid, pid>
这样的map来记录这二者的映射。具体是通过:+ +
/** Array of buffer pool pages. */
Page *pages_; // <fid, pid>
/** Page table for keeping track of buffer pool pages. */
std::unordered_map<page_id_t, frame_id_t> page_table_; // <pid, fid>Each
+Page
object also maintains a counter for the number of threads that have “pinned” that page. YourBufferPoolManager
is not allowed to free aPage
that is pinned. 有引用计数机制Each
+Page
object also keeps track of whether it is dirty or not. It is your job to record whether a page was modified before it is unpinned. YourBufferPoolManager
must write the contents of a dirtyPage
back to disk before that object can be reused.需要track dirty,并且这是你要干的;要写回,这也是你要干的Your
+BufferPoolManager
implementation will use theLRUKReplacer
class that you created in the previous steps of this assignment. TheLRUKReplacer
will keep track of whenPage
objects are accessed so that it can decide which one to evict when it must free a frame to make room for copying a new physical page from disk. When mappingpage_id
toframe_id
in theBufferPoolManager
, again be warned that STL containers are not thread-safe.
task2说得比较复杂,实现的函数较多,实际coding细节也比较繁琐,但debug倒是很轻松。
+主要内容就是实现BufferPoolManager
,在task1实现的LRU-K算法的基础上,写具体的内存换入换出的接口逻辑。
再次回顾我们整个project1的目的:实现一个从磁盘到内存的buffer。task1只是实现了一个内存页换入换出的LRU-K算法部分,task2则基于算法部分,实现了具体与上层交互的像样的逻辑。
+我认为这其中一个亮点就是,它非常完美地将LRU-K算法和具体的上层逻辑进行了解耦。LRU-K只需关注如何将这一堆freme_id
组织起来组织好,而无需关心具体内存页存放在哪,以及对应frame淘汰之后内存页又何去何从,因为这些逻辑都会由上层实现;而上层逻辑也无需关心具体的淘汰页算法【LRU-K/LRU/LFU,只需替换replacer_就可以替换换入换出策略】,而只需打好evictable标记,并且在调用evict方法之后做好后处理(如内存释放等等等)即可。
这其中有一个小细节也值得借鉴,即从page_id_
到frame_id_
的转化。frame_id_
有界,比较方便LRU-K算法实现,并且进行了LRU-K算法的容量控制,同时由于算法和上层逻辑的容量相同,故而也是pages_
的索引号;而page_id_
不能有界,因为实际上访问到的物理页不可能只共享pool_size_
个序列号。故而在这样解耦实现的基础上,二者缺一不可。
还有frame_id_
的复用,它是采用了类似我们日常生活中取号那样,要用号时从队列头取,不用号时塞回队列尾就行,这种方式我觉得还挺有意思。
其他部分虽然步骤繁杂,但理解难度不高,而且它提示得也很保姆了,所以不多bb。
+确实算简单了,我主要倒在没有认真看它的需求,这应该是语文问题(绷
+一个是FetchPage
这里:
如果所求物理页存在于buffer pool,直接返回+record access即可,不用再写回+读入。因为它的提示这边:
+这个是句号。也就是说后面那些写回啊read啊,是没找到时才做的,不是并列关系。
+这也很合理,毕竟你找到所需页就说明不用从磁盘读入,也即找到所需页=直接返回即可。
+另一个是UnpinPage
这里:
不应该写is_dirty_ = is_dirty
,因为它的提示这边:
可见参数is_dirty
为true是需要设置为dirty,为false的话没有别的意义,保持原来值就行。
还有一个就是,在Page
类中声明了friend:
故而BufferPoolManager
可以直接访问Page
的私有成员变量,而无需手动为Page
添加Getter/Setter方法。
这是要写我们在上面用的那个PageGuard?这让我想起了Lab0的ValueGuard
。
template <class T> |
不过其实这两个是不一样的。本次要实现的Page Guard的语义更类似lock_guard
。
++我们需要手动调用
+UnpinPage
,但这中就跟new/delete、malloc/free一样都要靠人脑来记住,不大安全。You will implement
+BasicPageGuard
which store the pointers toBufferPoolManager
andPage
objects. A page guard ensures thatUnpinPage
is called on the correspondingPage
object as soon as it goes out of scope. 【也许这需要在析构函数中实现?】Note that it should still expose a method for a programmer to manually unpin the page.仍然需要提供UnPin方法。As
+BasicPageGuard
hides the underlyingPage
pointer, it can also provide read-only/write data APIs that provide compile-time checks to ensure that theis_dirty
flag is set correctly for each use case.这个思想很值得学习。In the future projects, multiple threads will be reading and writing from the same pages, thus reader-writer latches are required to ensure the correctness of the data. Note that in the
+Page
class, there are relevant latching methods for this purpose. Similar to unpinning of a page, a programmer can forget to unlatch a page after use. To mitigate the problem, you will implementReadPageGuard
andWritePageGuard
which automatically unlatch the pages as soon as they go out of scope.
怎么说,其实只用仔细看相关文档和它的要求就不难,但你懂的我的尿性就是不细看文档,所以这里我也用gdb调了蛮久才过的。正确思路没什么好说的,直接记录下我觉得比较有意义的错误吧。
+在这个用例中,退出“}”会调用两次析构函数。
+我在coding的过程中,遇到了一个很神奇的死锁现象。
+在这里page->WLatch();
这句会死锁,而且还是在第一次调用FetchWritePage()
时死锁的:
WritePageGuard(BufferPoolManager *bpm, Page *page) : guard_(bpm, page) { |
但是添加了一句page->WUnlatch();
:
WritePageGuard(BufferPoolManager *bpm, Page *page) : guard_(bpm, page) { |
它就不会死锁了。
+这很奇怪,到底是发生了什么?我用GDB调了半天,在RWLatch.WLock()
处打了断点,也没发现在这之前有调用过lock()。于是我就去看了下std::shared_mutex
的官方文档(当然,这中间想了很久也不知道怎么办):
我就怀疑是不是我哪里写错了,所以就干了这种undefined的事,然后就导致死锁了。于是我写了个测试程序:
+发现,当在调用WLock
(也即std::shared_mutex::lock()
)之前,如果多调了一次XUnlock
(也即std::shared_mutex::unlock()
或者std::shared_mutex::unlock_shared()
),就会卡住。
这说明确实发生了不匹配问题。于是我就在Page
中添加了两个成员变量用来记录上锁和解锁的次数,并且在gurad test中打印了出来,结果发现:
确实发生了不匹配问题,是在这里:
+之后用gdb调下就发现错误了,不赘述了。
+在出现死锁问题时,我是想着,会不会是测试程序中,对同一页获取了一次ReadGuardPage
对象之后,再对同一页获取Read/WriteGuardPage
导致的呢?于是我就开始思考如何防范这个流程,最后写下了这样的代码:
auto BufferPoolManager::FetchPageRead(page_id_t page_id) -> ReadPageGuard { |
但很遗憾的是,我发现是无法区分当前进程持有write还是read锁的。也许有别的办法但我没想起来。
+总之,我认为这段代码还是很有参考价值的,姑且放着先。
+++参考:
+CMU 15-445 2023 P1 优化攻略 [rank#3] 写得非常细致,思路很清晰
+ +
++我的实现有一些并发小问题,详见lab2的并发部分~
+
lru-k的算法优化是自己想的,并行IO的优化思路全部来自 CMU 15-445 Project 1 (Spring 2023) 优化记录,我只是把这位大佬的思路自己实现了一遍。感觉还是太菜了,面对这种实际场景毫无还手之力一点思路没有QAQ但正是如此,这个细粒度化锁的小task才值得学习。
+放上优化前后性能对比:
+++In the leaderboard test, we will have multiple threads accessing the pages on the disk. There are two types of threads running in the benchmark:在具体的benchtest中,可以分为两类线程。
++
+- Scan threads. Each scan thread will update all pages on the disk sequentially. There will be 8 scan threads.
+- Get threads. Each get thread will randomly select a page for access using the zipfian distribution. There will be 8 get threads.
+Given that get workload is skewed(有偏向性的)(i.e., some pages are more frequently accessed than others), you can design your LRU-k replacer to take page access type into consideration, so as to reduce page miss.
+
我们可以回想起当初选择LRU-K而不选择LRU算法的原因:缓存污染。
++ ++缓存污染:
+LRU因为只需要一次访问就能成为最新鲜的数据,当出现很多偶发数据时,这些偶发的数据也会被当作最新鲜的,从而成为缓存。但其实这些偶发数据以后并不会是被经常访问的。
+
而在这里也是同理。我们的benchtest中,scan线程是顺序地访问磁盘上所有页,而get线程是遵从zip分布地访问,显然get线程的access记录比scan线程的有价值的多,并且scan线程的数据是很容易污染get线程的。
+所以,我的解决方法是,如果某个页被第一次访问,且该访问方式为SCAN,则RecordAccess进入历史访问队列;如果某个页不是被第一次访问,且访问方式为SCAN,则不做任何处理。不用修改UnpinPage的处理方式。
+++Instead of holding a global lock when accessing the disk manager【不要在访问
+disk_manager_
的时候使用bpm的全局锁latch_
】, you can issue multiple requests to the disk manager at the same time. This optimization will be very useful in modern storage devices, where concurrent access to the disk can make better use of the disk bandwidth.
详细的解决方法大佬这边已经说得很清楚了,接下来我就对其总体的做法进行一点总结,加上一些个人理解。
+我刚看到这个需求的时候是这么做的:
+if (pages_[fid].IsDirty()) { |
也即在原来代码的基础上做简单的改动,每次执行到涉及磁盘读写的地方,就暂时地开一下锁。但其实这样是不行的,当多个线程访问bpm,线程A在这里开锁执行Write,线程B正好得到锁,然后对pages_[fid]
执行比如说ResetMemory操作,这样就寄了。
所以,在磁盘读写的时候,我们仍然需要使用锁保护,只不过我们需要选择粒度更细的锁。这时我们就可以想到在page_guard
里常用的page自带的锁。在这里用page锁,既能够锁保护,又符合语义,看起来非常完美:
pages_[fid].WLatch(); |
但由于我们在returnpage_guard
的时候会获取锁,因而在这样的情况下,会发生死锁:
auto reader_guard_1 = bpm->FetchPageRead(page_id_temp); |
++在这里我们首先获取
+reader_guard_1
,持有了该 page 的读锁,并允许其他线程读;但在获取reader_guard_2
时,FetchPage
会在释放 bpm 写锁前,请求该 page 的写锁;但由于reader_guard_1
已经申请了该 page 的读锁,就会造成死锁,与预期结果不符。
因而,我们就可以选择在bpm内部,单独为pages_数组的每一页都维护一个锁,在每个对page页属性进行读写的地方进行锁定:
+std::shared_mutex latch_; |
然后对代码进行重排序,尽量分离bpm内部成员和page内部成员属性的修改:(以FetchPage
为例)
auto BufferPoolManager::FetchPage(page_id_t page_id, [[maybe_unused]] AccessType access_type) -> Page * { |
其他地方也是一样。就不多赘述了。
+当外界需要对页进行读写时,需要使用page自带的锁;而当bpm内部需要对页进行读写时,则使用的是bpm内部自带的页锁。
+这句话说完,相信危险性已经显而易见了:我们使用了两把不同的锁维护了同一个变量!而且可能会有两个线程分别持有这两个锁,对这个变量并发更新!
+但其实,在当前这个场景,这么做是没问题的。
+外界实质上只能对page的data字段进行读写。因而,有上述危险的,实质上就只有bpm中会对data字段进行改变的地方,也即bpm::NewPage()
、bpm::FetchPage()
、bpm::DeletePage()
这三个地方。
而在前两个地方,我们会使用到的page都是闲置/已经被释放的页,因而外界不可能,也即不可能有别的线程,会持有page的锁并且对其修改;同样的,在第三个地方,我们会使用的page也是pincount==0的页,仅有当前线程在对其进行读写。
+因而,综上所述,这样做是并发安全的。
+]]>相比于fall2022(Trie),spring2023(COW-Trie)的难度更大,算法更复杂【毕竟是要实现一个cow的数据结构】,我认为两个都很有意义,故而两个都做了。
+其中在Trie中,由于我是第一次接触cpp,所以遇到了很多麻烦。好在经过18h+的cpp拷打后,cow-trie虽然难度大,语法也更复杂一些,但我还是很快(话虽如此也花了7、8小时23333)就完美pass了。不过效率可能还是不大高,毕竟我不熟悉cpp,很多地方可能都直接拷贝了emm希望后续学习可以加把劲。
+++In this project, you will implement a key-value store backed by a concurrent trie. 实现并发安全的trie
+To simplify the explaination, we will assume tha the keys are all non-empty variable-length strings but in practice they can be any arbitrary type. key为非空变长字符串
+The key-value store you will implement can store string keys mapped to values of any type. key必须是字符串类型,但value可以是任意类型
+The value of a key is stored in the node representing the last character of that key.
++
本次实验完成时间总计18h+。是的,lab0就做了这么久【难绷】
+其实光就实验内容来看,无非就是实现trie树,算法上没有很难,最难的应该是Remove函数的编写,因为它是个递归。
+但正如本次实验的主题C++ Primer
所揭示的那样,本次实验的真正难点在于C++……而在接触本实验之前,我对c++一无所知。
除了这个萌新debuf之外,我还不小心犯了另一件非常sb的乌龙,加上对cpp实在是太小白了,再加上这几天破事又贼多,更是让我心态大崩,差点一蹶不振不想写了(。
+因而,整个实验在我看来十分痛苦。coding阶段,就是 语法错误-看了半天报错信息才发现哪错了-改错误-改得不对-再改-再改-再改……这样的痛苦过程在循环往复;运行阶段,就是看着stack trace发呆、用gdb调来调去还不知道为什么错了这样的痛苦过程在循环往复。好在,我还是坚持下来了,虽然内心还是很浮躁很浮躁(
+不过总而言之,我认为这次实验给我收获挺大的。它帮助我熟悉了C++,但我认为更重要的,是它帮我矫正了心态。做这个实验之前,我内心是很浮躁的(那会破事太多了),而且因为它是lab0所以有点轻敌(对不起。。),因而我所采取的策略是“错误驱动”,也即哪里报错就百度下怎么改就行。这样的心态就导致我的debug过程极度痛苦,因为完全看不懂报错信息,压根不知道错在哪里,百度也百度不出来。于是我被迫修改了战略,去看了我一直不想看的书,学了我一直很害怕的cpp,用了我一直很抗拒的gdb调试,才发现其实都没有我想象的这么恐怖。这期间、这几天的种种心路历程,我认为是十分可贵的。
+我下载下来starter code的时候,发现找不到它要我们实现的p0_trie.h
,只有这几个:
我便觉得可能是实验代码改版了。但是我并没有多想,我觉得可能只是代码模板改版了但实验内容不变QAQ【为什么会这么觉得呢?因为我看到指导书的url为fall2022便以为这是最新版指导书,没有想到春季学期也可以开课,还有个spring2023呃呃】而且代码看起来也确实是要我们实现Trie树【虽然跟指导书说得不大一样】。故而,我就这么直接开干了。
+写完了Tire树的逻辑【这部分确实挺简单的】之后,我就开始了漫长的痛苦且折磨的原地兜圈之旅。由于真正的spring2023的代码模板是实现COW-Trie,故而代码模板中很多地方都使用了const关键字,包括树结点以及树的children_成员。
+// in class Trie |
如上是spring2023的代码模板。
+如果使用其给我们提供的COW-Tire接口来实现Trie树,就会产生巨大的矛盾。你无法在root_
的孩子中插入或者删除一个树节点,因为root_
指向一个const对象,其children_
域也是const的。同样的,你也无法对root_
的孩子的孩子集合做增删结点操作,因为它也是const的。
由于对C++不熟悉,通过满屏幕的报错从而搞清楚上面那些东西这件事,就花费了我很多很多时间。
+error: no matching function for call to |
比如说这个错误我就看了半天完全不知道啥意思(
+好在明白上面这点后,我很快就发现了spring2023的存在,然后切到了fall2022的正确分支【乐】
+经过了此乌龙后,我深刻地意识到了我对C++一窍不通的程度(,比如说上面的这些const,还有比如说&是什么东西&&又是什么东西,shared_ptr又是什么东西等等等,我都不懂。故而,我压制了内心的浮躁,去简单看了一下书,了解了new的作用、左值引用右值引用、move、智能指针这几个地方,然后再去重新开始写本实验,最终果然好了不少。
+在Trie::GetValue
中,我本来是这么写的:
std::unique_ptr<TrieNode>* t = &root_; |
这就会导致,tmp和(*t)会指向同一块内存区域,并且它们都是unique_ptr
。随后,代码块遇到}
结束,tmp的析构函数被调用,那块内存区域被free,但(*t)依然指向那块内存区域,随后在释放整个Trie树时这块区域就会被再次释放,然后寄(
有一个方法可以在不剥夺某个unique_ptr
的所有权的同时,又能用另一个变量来操作该指针所指向的对象。这个方法就是——使用指向unique_ptr
的指针(。
也即比如:std::unique_ptr<TrieNode> *
本次实验还格外要求了代码规范问题。
+make format |
我暂时没进行gradescope的自测,原因是它上面报了个跟我没啥关系的错,我不知道怎么改呃呃。
+In file included from /autograder/bustub/src/common/bustub_instance.cpp:17: |
都指向说找不到这个fort。但我真的不知道它为啥找不到,因为我看CMakeLists.txt中已经加了third_party/
这个include目录了,并且这个东西的路径也确实是third_party/libfort/lib/for.hpp
。
我还在CMackLists.txt
、src/CMackLists.txt
、tools/shell/CMackLists.txt
里面都加了include(${PROJECT_SOURCE_DIR}/third_party/libfort/lib/fort.hpp)
,但是依然报了这样的错:
它这为啥找不到我是真的很不理解。
+所以真的很奇怪。暂且先放着吧,之后有精力研究下这些编译链接过程。
+++CMU 15445 Project 0 (Spring 2023) 学习记录 参考了task2和一个bug
+
先放个通关截图~
+总体用时(coding+debug+note)10h+
+本次实验是在它给的接口的基础上,实现一株并发安全的cow的trie树,还有一个小小的实现upper
和lower
函数的实验用来熟悉我们之后要写的db的东西。算法难度还是有一些的,我的coding和debug时间估摸着可能有46开。
总体来说整个实验还是非常有价值的,相比往年难度和意义都更上了一层。感谢实验设计者让我做到设计得这么好的实验~
+++In this task, you will need to modify
+trie.h
andtrie.cpp
to implement a copy-on-write trie.下面举例说明
+Consider inserting
+("ad", 2)
in the above example. We create a newNode2
by reusing two of the child nodes from the original tree, and creating a new value node 2. (See figure below)+
If we then insert
+("b", 3)
, we will create a new root, a new node and reuse the previous nodes. In this way, we can get the content of the trie before and after each insertion operation. As long as we have the root object (Trie
class), we can access the data inside the trie at that time. (See figure below)+
One more example: if we then insert
+("a", "abc")
and remove("ab", 1)
, we can get the below trie. Note that parent nodes can have values, and you will need to purge all unnecessary nodes after removal.+
To create a new node, you should use the
+Clone
function on theTrieNode
class. To reuse an existing node in the new trie, you can copystd::shared_ptr<TrieNode>
: copying a shared pointer doesn’t copy the underlying data.You should not manually allocate memory by using
+new
anddelete
in this project.std::shared_ptr
will deallocate the object when no one has a reference to the underlying object.
task1的目标就是实现我们的cow-trie的主体,先不要求并发。
+虽说算法上比较复杂,但是由于它图解以及代码中的注释解说都已经说得很详细了,再加上之前已经写过了trie树有一个大体框架,因而具体coding的时候思路还是比较清晰的。
+我认为具体的难点还是在于cpp上。下面列出了几个比较有价值的错误和相关debug过程,其中const转移
和显示保存is_value_node_
是我认为两个比较难的点。
在trie.h
中:
class Trie { |
可以看到,为了呼应我们的cow-trie,在语法上强制性要求不能“directly modify”,它将root_
和children_->second
同时设置为了一个指向对象为const的指针。而这意味着什么呢?意味着我们不能修改root_
的内容,也不能修改root_->children_->second
的内容,同样的孩子的孩子也不行。这就需要我们在Put
方法中遍历trie时,对遍历路径上的每个结点都需要copy一次,故而我们的代码具体是如下实现的:
首先,利用TrieNode::Clone()
方法来创造一个非const指针的新root:
// in trie.h TrieNode{} |
// 创造新的根节点,并且为非const类型 |
再然后,每次迭代的时候在遍历路径上创造新的结点,结点类型非const;再利用shared_ptr的共享复制( t = tmp;
),就能使得当前的t指针一直保持非const状态。
for (uint64_t i = 0; i < key.length(); i++) { |
++注:我本来的写法是这样的:
++ +
for (uint64_t i = 0; i < key.length(); i++) {
auto it = t->children_.find(key.at(i));
if (it == t->children_.end()) {
if (i != key.length() - 1) {
std::shared_ptr<TrieNode> tmp(new TrieNode());
// ...
} else {
std::shared_ptr<TrieNodeWithValue<T>> tmp(new TrieNodeWithValue(std::make_shared<T>(std::move(value))));
// ...
break;
}
} else {
if (i == key.length() - 1) {
std::shared_ptr<TrieNodeWithValue<T>> node =
std::make_shared<TrieNodeWithValue<T>>(it->second->children_, std::make_shared<T>(std::move(value)));
// ...
break;
}
std::shared_ptr<TrieNode> node = std::shared_ptr<TrieNode>(it->second->Clone());
// ...
}
it = t->children_.find(key.at(i));
t = it->second;
}也就是为了省事,将t指针的转移集中放在了循环体最后进行。但这样是不行的。
+cpp中,可以将非const对象自然转移为const对象,比如代码中就将非const的新结点放进了
+children_
中;但是不允许将const对象自然转移为非const对象,比如代码中的t = it->second;
。因而,我们对t指针的转移不能在新结点放入其children_
之后。
++注2:在这里,我本来还多用了一个prev指针,因为在coding的时候用的是上面的本来的写法,误以为t指针只能是const,所以还得有父节点才能再把t指针复制一遍。但其实并非如此,而且就算如此prev指针也还是跟t指针一样的const的。。。不过还好编译前发现了上面那点改过来了,要不然就得面对编译大报错2333
+
make_shared
作用也类似于new,会在堆上开辟空间用以存放共享指针的base对象。这也让我想起来我在做上面那个实验时一个地方改成make_shared
就对了,估计是犯了用栈中对象创建共享指针的错误。
++官方鼓励用
+make_shared
函数来创建对象,而不要手动去new。这一是因为,new出来的类型是原始指针,make_shared
可以防止我们去使用原始指针创建多个引用计数体系;二是因为,make_shared
可以防止内存碎片化。
在写这样的shared_ptr的共享转移时:
+std::shared_ptr<TrieNode> tmp = make_shared<TrieNode>(); |
会在t=tmp
这里报错不能把int类型的tmp复制给t。我看了半天很奇怪哪来的int类型,查了半天怎么共享shared_ptr,最后才发现是因为这里:
std::make_shared<TrieNode>() |
漏了个std::
呃呃。
在trie.cpp RemoveHelper()
中:
if (i != key.length() - 1) { |
否则会:
+查看trie_test.cpp
的代码:
TEST(TrieTest, BasicRemoveTest2) { |
它是在ASSERT_EQ(trie.Get<uint32_t>("te"), nullptr);
这句报错的。这确实很奇怪,因为“te”已经被remove了。这是为什么呢?
经过gdb调试,trie的Remove和Put功能都确实很正常,但是我发现了一个诡异的现象。
+在经过trie = trie.Remove("te");
这句话后,trie的状态是t-e-(s)-(t)【括号表示为有值结点,类型为TireNodeWithValue
】,符合预期。但是,经过紧随其后的trie = trie.Remove("tes");
之后,trie的状态却变成了t-(e)-s-(t)。
这实在是很诡异,为什么经过了一次Remove之后,trie = trie.Remove("te");
这句话的效果就被重置了?
我想了挺久,最终认为这是构造方法的问题。
+再次看一遍我们的Remove的代码:
+if (i != key.length() - 1) { |
以及TrieNodeWithValue::Clone()
:
auto Clone() const -> std::unique_ptr<TrieNode> override { |
以及Clone()方法调用的TrieNodeWithValue
的构造方法:
explicit TrieNodeWithValue(std::shared_ptr<T> value) : value_(std::move(value)) { this->is_value_node_ = true; } |
可以发现,在多态作用下,e结点始终是一个TrieNodeWithValue
的结点。
在我们去除tes这个key时,会到这个分支:
+if (i != key.length() - 1) { |
Clone()中会调用node->second
,也即e结点的构造方法,然后将e结点的is_value_node_
设置为true,从而导致Get
中无法通过这句代码返回nullptr。
if (!(t->is_value_node_)) { |
因而,为了解决这个问题,我们就需要暂存is_value_node_
,并在之后恢复它。
++In this task, you will need to modify
+trie_store.h
andtrie_store.cpp
.需要实现并发安全版本。For the original Trie class, everytime we modify the trie, we need to get the new root to access the new content. But for the concurrent key-value store, the
+put
anddelete
methods do not have a return value. This requires you to use concurrency primitives to synchronize reads and writes so that no data is lost through the process.在并发安全版本中,Put
和Get
不会返回trie,而是应该修改包装类的base trie。Your concurrent key-value store should concurrently serve multiple readers and a single writer. That is to say, when someone is modifying the trie, reads can still be performed on the old root. When someone is reading, writes can still be performed without waiting for reads.同一时刻可以有一个writer和多个reader。
+Also, if we get a reference to a value from the trie, we should be able to access it no matter how we modify the trie. The
+Get
function fromTrie
only returns a pointer. If the trie node storing this value has been removed, the pointer will be dangling. Therefore, inTrieStore
, we return aValueGuard
which stores both a reference to the value and the TrieNode corresponding to the root of the trie structure, so that the value can be accessed as we store theValueGuard
.为我们提供了ValueGuard
用以确保return值长时间有效。To achieve this, we have provided you with the pseudo code for
+TrieStore::Get
intrie_store.cpp
. Please read it carefully and think of how to implementTrieStore::Put
andTrieStore::Remove
.我们在Get
方法中给出了详细的步骤引导。你需要依据它来对Put
和Get
进行修改。
task2的内容是实现cow-trie并发安全版本的包装类TrieStore
。
相比于fall2022的并发内容,由于加上了cow的特性,本次实验更加复杂。我写了三版都没写对,看到别人的才豁然开朗(很遗憾没有自己再多想会儿……)接下来就从我的错误版本开始,逐步过渡到正确版本吧。
+Get
的实现很简单,按他说的一步步做就行,在这边不做赘述。Put
和Remove
思路差不多,在此只放Put
的代码。
这样看起来很合理:同一时刻似乎确实只有一个writer对root_进行修改,也似乎确实同时可以有别的线程获取root_lock_对其进行读取。但其实,前者是错误的。
+假如说进程A和进程B都在Put
逻辑中。进程A执行到了root_ = new_trie
这句话,然后进程B进入到root_.Put
中。
root_ = new_trie
使用了运算符=的默认实现,进行浅拷贝,故而会修改root_->root_
;root_.Put
中会对root_->root_
进行移动。
进程B在Put中执行std::move(root_)
之后,进程A又让root_->root_
变成了别的值(trie浅拷贝),导致原来的root_的引用计数变为0,自动释放(因为是智能指针shared_ptr),进程B在Put中再次访问就会寄。
++注,此处是因为智能指针引用计数为零才释放的,cpp没有垃圾回收机制。
+
void TrieStore::Put(std::string_view key, T value) { |
版本1错误后,我发现我并没有按它强调的“somehow similar to Get”那样,模仿Get
中的写法来做。于是我就修改了下,版本2诞生了。
但是这样的话,依然不能解决版本1中的问题。所以我又搞了个版本3.
+void TrieStore::Put(std::string_view key, T value) { |
这样就能通过所有测试了。
+但这样做虽然能解决多个writer的争夺问题,但不能解决一个writer和一个reader的争夺问题:因为两者都争夺同一个root_变量,但只有reader争夺root_lock_,这显然很不安全。因而,终极版本应该是这样:
+template <class T> |
可以看到整个思维过程是线性的,逐步改进下来,正确答案其实很容易想到。只可惜我太浮躁了,没有静下心来好好想,在版本3之后就去看了眼别人怎么写的(罪过)没有独立思考,算是一个小遗憾。
+一个考查我们debug入门技巧的小任务,简单,但我觉得形势很新颖。
+随便贴点debug过程的截图。
+任务中,需要获取root_的孙子。所以我就这么写了个gdb指令:p root_->children_.find('9')->second
,然后就爆出了标题这个错误。
百度了下看到了这个:
++ +++
也许是因为我们通过.访问了children_的成员find吧(
+总之,我最后是在trie_debug_test
添加了这几行代码解决的:
// Put a breakpoint here. |
也即添加了it和val,以及防止unused报错的cout语句。gdb调试时打印it和val就行。
+++来自CMU 15445 Project 0 (Spring 2023) 学习记录
+在我本地的环境上,调试三问的答案分别是
+8 1 42
,但该答案无法通过 Grade 平台的评测。发现在 Discord 上有人提出了同样的问题,助教 Alex Chi 给出了解答:++Alex Chi — 2023/02/15 23:29
+
It is possible that your environment produces different random numbers than the grading environment. In case your environment is producing different set of random numbers than our grader, replace your TrieDebugger test with:+
auto trie = Trie();
trie = trie.Put<uint32_t>("65", 25);
trie = trie.Put<uint32_t>("61", 65);
trie = trie.Put<uint32_t>("82", 84);
trie = trie.Put<uint32_t>("2", 42);
trie = trie.Put<uint32_t>("16", 67);
trie = trie.Put<uint32_t>("94", 53);
trie = trie.Put<uint32_t>("20", 35);
trie = trie.Put<uint32_t>("3", 57);
trie = trie.Put<uint32_t>("93", 30);
trie = trie.Put<uint32_t>("75", 29);
难绷,我反复确认了好几遍(。主要还是太相信cmu的权威了,觉得这实验都发布了好几个月了应该不会有错,就没想到是这个问题。我觉得最好还是把这个问题反应在指导书上吧。
+++Now it is time to dive into BusTub itself!
+You will need to implement
+upper
andlower
SQL functions.This can be done in 2 steps:
++
+- implement the function logic in
+string_expression.h
.- register the function in BusTub, so that the SQL framework can call your function when the user executes a SQL, in
+plan_func_call.cpp
.To test your implementation, you can use
+bustub-shell
:+
cd build
make -j`nproc` shell
./bin/bustub-shell
select upper('AbCd'), lower('AbCd');
ABCD abcd
说实话乍一看我还没看懂(。它放在这个位置,我还以为跟上面实现的cow-trie有什么关系,并且误以为这个upper和lower是什么上层接口底层接口的意思,跟它大眼瞪小眼了半天。直到看到了下面的案例,才发现跟trie似乎没有任何关系23333
+本次实验内容其实就是实现sql的转换大小写的函数。知道了要做什么之后,任务就很简单了,按着它提示一步步做就行。
+不过此task重点其实也是在稍微了解下我们接下来要打交道的sql框架的代码。比如说,此次我们的实现涉及到的,居然是一个差不多是工厂模式(其实更像策略模式?)的一部分:
+外界传入想调用的函数名,通过GetFuncCallFromFactory
获取对应的处理对象
得到处理对象后调用其Compute
方法就行
第一次如此鲜明地看到一个设计模式在cpp的应用,真是让我非常震撼。
+依旧是这三件套:
+make format |
执行了该命令:cmake -DCMAKE_BUILD_TYPE=Debug -DBUSTUB_SANITIZER= ..
之后,执行make
报错missing argument to '-fsanitize='
发生这个的原因是cmake的命令中将BUSTUB_SANITIZER
设置成了空。解决方法就是将其设置为别的值就好了,具体想设置成什么值可以参见:关于GCC/LLVM编译器中的sanitize选项用处用法详解 我这里姑且随便设置了个leak
。
TODO,注意一下为什么每个executor的child executor的&&和&的差别
+++In this project, you will implement the components that allow BusTub to execute queries. You will create the operator executors that execute SQL queries and implement optimizer rules to transform query plans.
+实现SQL查询的执行,并且实现语句优化。
+
++In this project, you will add new operator executors and query optimizations to BusTub.
+BusTub uses the iterator (i.e., Volcano) query processing model, in which every executor implements a
+Next
function to get the next tuple result.When the DBMS invokes an executor’s
+Next
function, the executor returns either:(1) a single tuple
+ In BusTub’s implementation of the iterator model, 除了元组外还会返回record identifier (
+RID
)(2) an indicator that there are no more tuples.
+With this approach, each executor implements a loop that continues calling
+Next
on its children to retrieve tuples and process them one by one.
介绍完了bustub的框架之后,它对通过语法树进行查询优化进行了详细的样例介绍。
+首先温习一下什么是语法树(abstract syntax tree, AST ):
+SQL语句
+Select `title` |
其语法树表示+优化结果如下图所示:
+算法如下,其关键思路就是选择投影尽早做,能移多下去就移多下去
+而这里15445介绍的也是这样的语法树优化算法。
+首先记录一下它这几个专有名词对应的操作:
++++
+- Projection:投影
+- Filter:选择
+- MockScan:对一个表进行的扫描操作
+- Aggregation:聚合函数
+- NestedLoopJoin:嵌套循环连接
+
再结合它给的几个语法树的例子:
+SELECT * FROM __mock_table_1; |
SELECT colA, MAX(colB) FROM |
SELECT * FROM __mock_table_1 WHERE colA > 1; |
values (1, 2, 'a'), (3, 4, 'b'); |
可以看到,它大概是用缩进来表示了AST的父子关系。
+我们课上学习的语法树中每个table标志对应着一个MockScan;笛卡尔积+选择操作可以表示为一个NestedLoopJoin。
+对于这些输出的意义,指导书也给了详细的解释:
+ColumnValueExpression
+也即类似exprs=[#0.0, #0.1]
,#0
意为第一个子节点(不是第一个表的意思。。)
++火山模型和优化(向量化执行、编译执行) 这篇文章写得很详细,下文也摘抄自该博客。
+ +
火山模型又称 Volcano Model 或者 Pipeline Model(或者迭代器模型)。该计算模型将关系代数中每一种操作抽象为一个 Operator,将整个 SQL 构建成一个 Operator 树,从根节点到叶子结点自上而下地递归调用 next() 函数。
+一般Operator的next() 接口实现分为三步:
+因此,查询执行时会由查询树自顶向下的调用next() 接口,数据则自底向上的被拉取处理。火山模型的这种处理方式也称为拉取执行模型(Pull Based)。
+大多数关系型数据库都是使用迭代模型的,如 SQLite、MongoDB、Impala、DB2、SQLServer、Greenplum、PostgreSQL、Oracle、MySQL 等。
+火山模型的优点是,处理逻辑清晰,简单,每个Operator 只要关心自己的处理逻辑即可,耦合性低。但是缺点也非常明显:
+每处理一行需要调用多次next() 函数,而next()为虚函数,开销大。
+编译器无法对虚函数进行inline优化,同时也带来分支预测的开销,且很容易预测失败,导致CPU流水线执行混乱。
+数据以行为单位进行处理,不利于CPU cache 发挥作用。
+火山模型显而易见是以从上到下一个流水线形式执行的,它的最理想情况是每个流水线节点所需的这个tuple都存储在寄存器中。然而,有一些操作,如聚合函数等等,需要对整个表进行操作才能获取到当前所需tuple,而整个表显然最多只能读入到内存中,这样的操作就被称为pipeline breaker。
+下面的实现中的aggregation、sort、hash join的build阶段都是pipeline breaker,这些复杂的操作阶段都需要在init()函数中进行。
+TODO,从宏观整个架构简介
+++The entirety of the catalog implementation is in
+src/include/catalog/catalog.h
. You should pay particular attention to the member functionsCatalog::GetTable()
andCatalog::GetIndex()
. You will use these functions in the implementation of your executors to query the catalog for tables and indexes.
它意思大概是说在实现executor时可能需要用到catelog里这两个函数。
+GetTable
返回一个TableInfo
:
struct TableInfo { |
++For the table modification executors (
+InsertExecutor
,UpdateExecutor
, andDeleteExecutor
) you must modify all indexes for the table targeted by the operation. You may find theCatalog::GetTableIndexes()
function useful for querying all of the indexes defined for a particular table. Once you have theIndexInfo
instance for each of the table’s indexes, you can invoke index modification operations on the underlying index structure.In this project, we use your implementation of B+ Tree Index from Project 2 as the underlying data structure for all index operations. Therefore, successful completion of this project relies on a working implementation of the B+ Tree Index.
+
话说index是那个索引吗,就是每张表有几个建立在某个属性的索引,也即一张表可以有n棵b+树
+GetIndex
返回一个IndexInfo
:
struct IndexInfo { |
++The BusTub optimizer is a rule-based optimizer. Most optimizer rules construct optimized plans in a bottom-up way(自底向上). Because the query plan has this tree structure, before applying the optimizer rules to the current plan node, you want to first recursively apply the rules to its children.
+At each plan node, you should determine if the source plan structure matches the one you are trying to optimize, and then check the attributes in that plan to see if it can be optimized into the target optimized plan structure.
+In the public BusTub repository, we already provide the implementation of several optimizer rules. Please take a look at them as reference.
+
++In the background section above, we saw that the BusTub can already retrieve data from mock tables in
+SELECT
queries.This is implemented without real tables by using a
+MockScan
executor to always generate the same tuples using a predefined algorithm.This is why you cannot update these tables.
+
也就是说意思是目前的mockscan executor不是真的查表,而是返回固定的元组。
+看了一遍代码,感觉大概明白了。我们可以来看一下迭代器的Next函数:
+auto MockScanExecutor::Next(Tuple *tuple, RID *rid) -> bool { |
其核心就是调用func_来获取表的元组。
+也就是说是这样的,每个MockScanExecutor用来执行一个plan,那么也就对应着某一个table。通过执行某一个table特定的迭代function,就可以返回元组。
+这个迭代function比如说对于表tas_2023是这样的:
+if (table == "__mock_table_tas_2023") { |
也即MockScanExecutor负责对表指针的管理,function负责实际对表的物理访问。这样就成功解耦了。
+++In this task, you will implement executors that read from and write to tables in the storage system.
++
+- +
src/execution/seq_scan_executor.cpp
- +
src/execution/insert_executor.cpp
- +
src/execution/update_executor.cpp
- +
src/execution/delete_executor.cpp
- +
src/execution/index_scan_executor.cpp
而我们本次实验就是需要实现这么一大堆的executor。看来又是个体力活了。
+可以看到,前缀++重载的运算符方法和后缀++是不一样的。
+++这里我理解得还是肤浅了…… 根据 这篇文章,
+++i
的内部类定义为T& T:: operator++();
,而i++
的内部类定义为T T:: operator++(int);
[1],前置操作返回引用,后置操作返回值。后置操作的int
参数是一个虚拟参数,用于区分运算符++
的前置和后置。理论上,i++
会产生临时对象,实践中,编译器会对内置类型进行优化;而对于自定义类型(如这里的 Iterator),++i
的性能通常优于i++
。
值得一提的是它跟MockScan的关系。MockScan是一种模拟操作,所以各种表都是硬编码在它的mock_scan.h里的;而SeqScan就是真正的遍历操作了,它需要获取tuple就需要通过各种复杂的物理操作和封装一步步读取了。
+通过实现SeqScan,我们可以初步窥探整个bustub物理层面交互的架构。
+跟之前project中的索引entry一样,实际的数据tuple也保存在page中,其对应类为TablePage
。并且是堆文件组织结构:
+++
TablePage
的结构值得一提。在它的成员定义中,我们可以看到其中有两个柔性数组成员(Flexible array member):
++ +
char page_start_[0];
TupleInfo tuple_info_[0];之前的Project2,我们只接触过一个的case,这里的两个感觉其实也同理可得,相当于
+page_start_
和tuple_info_
都指向最末尾空闲空间的开始。+
TablePage
的实际存储结构如下:+ +
-> increase increase <-
| ------------------------------- | ********************************* |
↑ ↑
page_start & tuple_info +TUPLE_INFO_SIZE*sizeof(TupleInfo)也即tuple info存储在前半部分,tuple data存储在后半部分,并且二者增长方式相反。
+
而多页TablePage
就构成了一个TableHeap
,也即其物理存储空间。每次创建表时,我们就会分配对应的heap空间和相关meta data。TableHeap
对外提供了增删改查元组的方法,也提供了一个迭代器实现TableIterator
,用于遍历里面的元素。
而由于元组tuple存储在磁盘中,所以我们需要在读取它的值的时候先进行反序列化DeserializeFrom
,这个过程需要用到表的类型信息和offset信息之类的,所以Tuple::GetValue
需要传入schema
参数。
它基本原理也就是顺序遍历整张表,没什么好说的。
+在本次的sequence scan实现中,我们就需要首先获取表对应的iterator:
+// 巨长一串 |
然后通过这个iterator不断迭代获取元素即可。
+有一点要注意的,应该是对删除元组的处理,毕竟sequence scan算是是实现其他二级操作的基石了,所以我们必须在这里处理删除元组。具体逻辑如下:
+do { |
对于SQL的嵌套子查询,bustub采用的是递归实现。具体来说,以insertion为例:
+外界调用情况如下所示。
+// Execute a query plan. |
CreateExecutor
是一个递归函数,递归创建每个子查询的实例,把对应的executor返回给父查询
auto ExecutorFactory::CreateExecutor(...) |
然后我们再在父查询的Init中调用子查询的Init和Next等方法
+void InsertExecutor::Init() { |
如此,就能递归实现嵌套子查询。
+++The
+InsertExecutor
inserts tuples into a table and updates any affected indexes.The planner will ensure that the values have the same schema as the table. The executor will produce a single tuple of integer type as the output, indicating how many rows have been inserted into the table.
+
这里将Insert语句插入的值视为一个匿名子表,对其初始化后使用它的迭代器进行元素访问即可。
+bustub将一切表达式抽象为了这么几个类:
+AbstractExpression // 基类 |
而从UpdatePlanNode中,我们可以获取到update字句的所有表达式:
+/** The new expression at each column */ |
比如此处:
+bustub> explain (o,s) update test_1 set colB = 15445; |
然后我们分别计算每个expression的值,就可以获取更新之后的元组:
+// insert again |
删除元组的实现似乎只是简单地标记is_delete_
为true就好了。但是我在实际的代码实现(InsertTuple
)中似乎并没有看到重组删除空间or覆盖删除空间,每次插入页满只是简单地再申请新的一页,不会再回头。也许是为了简化起见暂不实现这个吧。
不过改进方法也很简单,对每个表进行固定分配页(或者说提供一个数据量达到百分之几的时候扩容的机制),然后页面间组织成环形链表,这样就能充分覆盖删除空间,同时也兼顾一定性能了。
+update的实现也不会很难,只需先删除原来的元组,再加个新元组即可。
+delete的实现完全照搬update就行,没什么好说的。
+++The
+IndexScanExecutor
iterates over an index to retrieveRIDs
for tuples. The operator then uses these RIDs to retrieve their tuples in the corresponding table. It then emits these tuples one at a time.You can test your index scan executor by
+SELECT FROM <table> ORDER BY <index column>
. We will explain whyORDER BY
can be transformed intoIndexScan
in Task #3. 哦吼,也就是说order-by会被翻译为index scan?那order-by的关键字如果不存在索引会怎么样,现建吗BusTub only supports indexes with a single, unique integer column. Our test cases will not contain duplicate keys. The type of the index object in the plan will always be
+BPlusTreeIndexForTwoIntegerColumn
in this project. You can safely cast it and store it in the executor object:+ +
using BPlusTreeIndexForTwoIntegerColumn = BPlusTreeIndex<IntegerKeyType, IntegerValueType, IntegerComparatorType>;
tree_ = dynamic_cast<BPlusTreeIndexForTwoIntegerColumn *>(index_info_->index_.get())但我看测试里怎么好像有两个键的index?
+You can then construct an index iterator from the index object, scan through all the keys and tuple IDs, lookup the tuple from the table heap, and emit all tuples in order.
+是的,project2的b+树实现确实只存了rid,然后我们通过rid就能知道实际的物理位置了
+
通过b+树组织索引结构,索引结点中存的是RID,RID可以用来指示tuple的物理位置,于是我们通过RID就可以获取到tuple,从而减少了磁盘IO。RID结构如下:
+// RID: Record Identifier |
并且,bustub保证了对于有索引的表,是不会有重复元组的,故而b+树实际上应该是一个稠密索引。
+(毕竟这个情况似乎有点复杂……物理存储上应该是按插入顺序顺序存储的,故而重复元组可能不放在一起,而我们实现的b+树又不支持重复结点,所以就会g。如果想要支持重复元组,可能就需要从两个改变思路入手,要么是修改b+树支持重复索引结点,此时b+树依然为稠密索引;要么是修改为链式存储结构以支持重复元组放在一起,此时b+树为稀疏索引。)
+非常非常崩溃,怎么保存索引尝试了很久都没做到:
+// 这样不行…… |
没办法,最终只能保存tree,iterator在next里动态获取了,我真是服了。等之后看完c++primer或者c++水平有所提升了再来解决这个问题吧。
+难绷,本来以为这个index-scan应该是最简单的,毕竟只用调用现成索引接口,没想到居然写了最久,可能足足两三个小时。。。
+首先的一个大难点就是如何保存迭代器了。在之前的seq-scan的时候,使用的是unique ptr,然而这里却不行会报一堆奇奇怪怪的错误(具体见一些想法-c++知识
)。最后只能换一个思路,不保存迭代器而是保存next_key_
了。然而又由于之前b+树的实现bug问题,导致对end iterator解引用是合法的,所以会产生各种奇奇怪怪的错误。解决了这个之后,之前写的insert、update、delete的更新索引部分又出了问题,rid和insert_key弄错了,诸如此类。
总之,解决了这一大堆小问题之后,才总算通过了index-scan的测试,真是令人南蚌。具体改了什么bug可以详情见b8d3ba546cfdea6fc576ad8d668322c87f6386c1
这个commit。
同时,也跟上面的sequence scan一样,都需要对标识为deleted的元组进行跳过处理。
+++这里我也是没想太多……事实上,index scan无需实时检测is_deleted字段并做处理,因为索引是会随着修改实时更新的,被删除的tuple不会在索引中。
+
++In this task you will add an aggregation executor, several join executors, and enable the optimizer to select between a nested loop join and hash join when planning a query.
+You will complete your implementation in the following files:
++
+- +
src/execution/aggregation_executor.cpp
- +
src/execution/nested_loop_join_executor.cpp
- +
src/execution/hash_join_executor.cpp
- +
src/optimizer/nlj_as_hash_join.cpp
++The
+AggregationPlanNode
is used to support queries like the following:+ +
EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA;
EXPLAIN SELECT COUNT(colA), mi(colB) FROM __mock_table_1;
EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA HAVING MAX(colB) > 10;
EXPLAIN SELECT DISTINCT colA, colB FROM __mock_table_1;也即聚合函数和DISTINCT、GROUP这种。
+
此处注意DINSTINCT也是通过aggregation实现的:
+EXPLAIN SELECT DISTINCT colA, colB FROM __mock_table_1; |
++The aggregation executor computes an aggregation function for each group of input. 作用于每一组
+It has exactly one child. The output schema consists of the group-by columns followed by the aggregation columns.
++ +
EXPLAIN SELECT colA, MIN(colB) FROM __mock_table_1 GROUP BY colA;
=== OPTIMIZER ===
// types标志聚合的种类,aggregates标识聚合的目标,group_by单独用于表示是否有group
Agg { types=[min], aggregates=[#0.1], group_by=[#0.0] } | (__mock_table_1.colA:INTEGER, <unnamed>:INTEGER)
MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
EXPLAIN SELECT COUNT(colA), min(colB) FROM __mock_table_1
=== OPTIMIZER === // 如果没有group,则其字段为空
Agg { types=[count, min], aggregates=[#0.0, #0.1], group_by=[] } | (<unnamed>:INTEGER, <unnamed>:INTEGER)
MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)As discussed in class, a common strategy for implementing aggregation is to use a hash table, with the group-by columns as the key.
+In this project, you may assume that the aggregation hash table fits in memory. This means that you do not need to implement a multi-stage, partition-based strategy, and the hash table does not need to be backed by buffer pool pages.
+也就是说这里采取的是基于hashtable的实现而非基于归并排序的,并且为了简单起见将hash table保存在内存中,所以无需进行多趟划分扫描。
+We provide a
+SimpleAggregationHashTable
data structure that exposes an in-memory hash table (std::unordered_map
) but with an interface designed for computing aggregations. This class also exposes anSimpleAggregationHashTable::Iterator
type that can be used to iterate through the hash table. You will need to complete theCombineAggregateValues
function for this class.
++Note: The aggregation executor itself won’t need to handle the
+HAVING
predicate. The planner will plan aggregations with aHAVING
clause as anAggregationPlanNode
followed by aFilterPlanNode
.
++Hint: In the context of a query plan, aggregations are pipeline breakers. This may influence the way that you use the
+AggregationExecutor::Init()
andAggregationExecutor::Next()
functions in your implementation. Carefully decide whether the build phase of the aggregation should be performed inAggregationExecutor::Init()
orAggregationExecutor::Next()
.
值得注意的是,这里的实现将COUNT(*)
和COUNT(colum)
区分开了:
enum class AggregationType { CountStarAggregate, CountAggregate }; |
因为这两者似乎语义上是有区别的,大概体现为以下几点:
+关于hashtable实现聚合的相关原理及相关示例,具体可见 这篇文章。感觉这系列文章都写得挺好的,如对TiDB有兴趣可以细看。
+++在 SQL 中,聚合操作对一组值执行计算,并返回单个值。TiDB 实现了 2 种聚合算法:Hash Aggregation 和 Stream Aggregation。
+在 Hash Aggregate 的计算过程中,我们需要维护一个 Hash 表,Hash 表的键为聚合计算的
+Group-By
列,值为聚合函数的中间结果sum
和count
。计算过程中,只需要根据每行输入数据计算出键,在 Hash 表中找到对应值进行更新即可。输入数据输入完后,扫描 Hash 表并计算,便可以得到最终结果。
+
故而思路也是很清晰了。我们在aggregation的实现中要做的,就是把child executor逐行喂给hashtable,最后再遍历hashtable得到结果即可。故而,我们重点需要实现hashtable的InsertCombine
函数和hashtable的iterator。
理解了hash-aggregation的算法原理后,代码逻辑方面就不算难了,其余最主要的难点应该是空值的处理。
+总结一下,bustub对空值的处理大概有以下几个要点:
+聚合函数对空值处理
+COUNT(*)
:计入空值
COUNT/MAX/MIN/SUM(v1)
:跳过空值
空值自身运算性质
+任意运算若有一个操作数为空,那么结果也为空。
+故而,当没有使用group by
关键字的时候(也即hashtable的key为空),此时不能天真地传入一个空的AggregationKey,而应该给它随便塞某个值。不然的话,hashtable内部的比较函数在处理空值的时候恒返回false,会导致检索失败。
空表情况处理
+当表为空的时候,要求:
+select COUNT(*), MAX(v1), COUNT(v1) from table_; |
这个操作我着实不懂为什么。。。所以我最终代码只能面向测试用例:
+if (!has_next && plan_->GetGroupBys().empty()) { |
++The DBMS will use
+NestedLoopJoinPlanNode
for all join operations, by default.You will need to implement an inner join and left join for the
+NestedLoopJoinExecutor
using the simple nested loop join algorithm from class.The output schema of this operator is all columns from the left table followed by all columns from the right table.
+For each tuple in the outer table, consider each tuple in the inner table and emit an output tuple if the join predicate is satisfied.
+
也即嵌套循环实现的join,与在课上学的sort merge join一样,都是古法join实现。
+nested join的实现相比之前的思路确实会复杂一些。我们需要学习如何迭代地调用Next
来实现一次嵌套循环。思路大概是这样:
Init(): |
然而其中有这几个细节需要进行处理:
+左连接的实现
+需要增加逻辑:当right遍历完之后,current_left_tuple_
仍未被组装进结果过,此时需要帮其拼接上空right tuple。
空表情况
+这个分支中:
+else: |
不能这样:
+else: |
这是为了防止空表情况,使得Move right一直返回false,导致之后checkPredict报空指针异常。
+测试要求left->Next()
调用次数与right->Init()
调用次数相同。
++这是为了强制让NestedLoopJoin的实现不是Pipeline Break,从而导致它性能垃圾了
+
++The DBMS can use
+HashJoinPlanNode
if a query contains a join with a conjunction of equi-conditions between two columns (equi-conditions are seperated byAND
).也就是说,当连接条件为一/多个列相等时,就可以用hash join。可以看到这是类似等值连接。
++ +
EXPLAIN SELECT * FROM __mock_table_1, __mock_table_3 WHERE colA = colE;
EXPLAIN SELECT * FROM __mock_table_1 INNER JOIN __mock_table_3 ON colA = colE;
EXPLAIN SELECT * FROM __mock_table_1 LEFT OUTER JOIN __mock_table_3 ON colA = colE;
EXPLAIN SELECT * FROM test_1 t1, test_2 t2 WHERE t1.colA = t2.colA AND t1.colB = t2.colC;
EXPLAIN SELECT * FROM test_1 t1 INNER JOIN test_2 t2 on t1.colA = t2.colA AND t2.colC = t1.colB;
EXPLAIN SELECT * FROM test_1 t1 LEFT OUTER JOIN test_2 t2 on t2.colA = t1.colA AND t2.colC = t1.colB;You will need to implement the inner join and left join for
+HashJoinExecutor
using the hash join algorithm from class.The output schema of this operator is all columns from the left table followed by all columns from the right table.
+As with aggregation, you may assume that the hash table used by the join fits entirely in memory.
+Hint: Your implementation should correctly handle the case where multiple tuples have hash collisions (on either side of the join). 必须正确处理哈希冲突的情况
+Hint: You will want to make use of the join key accessors functions
+GetLeftJoinKey()
andGetRightJoinKey()
in theHashJoinPlanNode
to construct the join keys for the left and right sides of the join, respectively.Hint: You will need a way to hash a tuple with multiple attributes in order to construct a unique key. As a starting point, take a look at how the
+SimpleAggregationHashTable
in theAggregationExecutor
implements this functionality. 可以参考SimpleAggregationHashTable
的实现Hint: As with aggregation, the build side of a hash join is a pipeline breaker. You should again consider whether the build phase of the hash join should be performed in
+HashJoinExecutor::Init()
orHashJoinExecutor::Next()
.
具体什么是hash join,可以参考 这篇文章。
+其大概思路也很简单,hash table就是一个map<key, vector<value>>
这样的数据结构,然后将两个输入的关系选举出一个小表作为Build(建立hash table),另一个作为Probe(扫描,并根据hash table->second进行迭代组合)。它其实就是一个精确了范围的nested loop join的变种,将nested里层的针对整个关系的大循环缩小为针对hash table一个bucket的小循环。
具体到这里,思路可以是这样的。首先为了简单起见,我们就不进行选举小表的判断了,固定将right child作为Build,left child作为Probe。建表的话,我们就简单粗暴地遍历right table,然后以right_key_expressions_
为keyTuple
为value直接建表(反正也是in-memory即可。。。)。然后之后,就仿照之前思路即可。
++Hash joins usually yield better performance than nested loop joins. You should modify the optimizer to transform a
+NestedLoopJoinPlanNode
into aHashJoinPlanNode
when it is possible to use a hash join.Specifically, the hash join algorithm can be used when a join predicate is a conjunction of equi-conditions between two columns. For the purpose of this project, handling a single equi-condition, and also two equi-conditions connected by
+AND
, will earn full credit.Consider the following example:
++ +
bustub> EXPLAIN (o) SELECT * FROM test_1 t1, test_2 t2 WHERE t1.colA = t2.colA AND t1.colB = t2.colC;Without applying the
+NLJAsHashJoin
optimizer rule, the plan may look like:+ +
NestedLoopJoin { type=Inner, predicate=((#0.0=#1.0)and(#0.1=#1.2)) }
SeqScan { table=test_1 }
SeqScan { table=test_2 }After applying the
+NLJAsHashJoin
optimizer rule, the left and right join key expressions will be extracted from the single join predicate in theNestedLoopJoinPlanNode
. The resulting plan will look like:+ +
HashJoin { type=Inner, left_key=[#0.0, #0.1], right_key=[#0.0, #0.2] }
SeqScan { table=test_1 }
SeqScan { table=test_2 }Note: Please check the Optimizer Rule Implementation Guide section for details on implementing an optimizer rule.
+Hint: Make sure to check which table the column belongs to for each side of the equi-condition. It is possible that the column from outer table is on the right side of the equi-condition. You may find
+ColumnValueExpression::GetTupleIdx
helpful.Hint: The order to apply optimizer rules matters. For example, you want to optimize NestedLoopJoin into HashJoin after filters and NestedLoopJoin have merged. 这个感觉可能意思就是说优化规则的优先级之类的吧,这里用了个例子说hash join的优先级一般得在filter和nested loop join合并了之后。
+Hint: At this point, you should pass SQLLogicTests - #14 to #15.
+
++The BusTub optimizer is a rule-based optimizer. Most optimizer rules construct optimized plans in a bottom-up way(自底向上). Because the query plan has this tree structure, before applying the optimizer rules to the current plan node, you want to first recursively apply the rules to its children. 基于规则的优化器,规则都是对语法树自底向上实施。感觉跟课内学的差不多。
+In the public BusTub repository, we already provide the implementation of several optimizer rules. Please take a look at them as reference.
+
在课程中学到的语法优化,应该也是基于规则的优化,具体见下图及之后列出的无穷无尽个定理:
+(本图新增了一条规则:选择+嵌套笛卡尔积=嵌套连接)
+查看目录src/optimizer/
,我们可以看到:
$ tree ../src/optimizer/ |
在本小节任务中,我们需要做的,就是参照其他的规则来实现nlj_as_hash_join
。但在此之前,我们不妨先研究一下它语法优化的总体架构。
auto Optimizer::OptimizeCustom(const AbstractPlanNodeRef &plan) -> AbstractPlanNodeRef { |
可以看到,它的实际原理很简单,就是按照这样的优先级顺序对语法树运用规则进行优化。
+以OptimizeMergeFilterNLJ
为例,我们可以研究一下它的整体架构:
auto Optimizer::OptimizeMergeFilterNLJ(const AbstractPlanNodeRef &plan) -> AbstractPlanNodeRef { |
可见,对语法树运用该merge filter nlj规则是采用自底向上的顺序,并且仅合并那些filter-笛卡尔积的结点。那么接下来,我们可以具体关注RewriteExpressionForJoin
的实现。
首先,我们需要明确bustub中对expression的抽象。以#0.0=#1.0
为例,expression的结构树如下所示:
每个叶子结点都是一个基本的expression类型,如column value、constant value等等等,整个子树构成一个其他expression类型,如comparation expr、arithmetic expr等等等。
+在未优化前,我们是先做笛卡尔积,再做选择。故而,假设t1有2列,t2有2列,选择条件为t1.col1 = t2.col4
,在未优化前,filter结点的expr将为:#0.0=#0.3
(两表经过笛卡尔积合在一起了)。故而,在RewriteExpressionForJoin
函数中,我们需要根据t1表和t2表分别的列数,将#0.0=#0.3
这样的表达式转化为#0.0=#1.1
这样的表达式(其实也就是只用处理所有类型为colum expr的叶结点即可)。而由于expression是递归结构,所以我们需要先针对其所有子节点进行处理。故而,RewriteExpressionForJoin
的实现如下:
auto Optimizer::RewriteExpressionForJoin(const AbstractExpressionRef &expr, size_t left_column_cnt, size_t right_column_cnt) -> AbstractExpressionRef { |
++Specifically, the hash join algorithm can be used when a join predicate is a conjunction of equi-conditions between two columns. For the purpose of this project, handling a single equi-condition, and also two equi-conditions connected by
+AND
, will earn full credit.
看完了merge filter nlj的实现之后,本次任务的实现就变得不那么困难了。
+当一个nlj的predicate条件是一堆使用AND连接的“=”expr,我们就可以将该nlj转化为hash join。而OptimizeNLJAsHashJoin
作用于OptimizeMergeFilterNLJ
之后,故而,我们可以直接对所有的nlj结点进行判定重写。
具体来说,我们可以首先实现一个函数CheckIfEquiConjunction
,给定expr结构树输入,判断其是否只由AND、”=”、”column expr”构成。这个过程还需要做一件事,就是分离出hash join所需要的key expression,如nlj的连接条件为#0.1=#1.2 AND #1.1=#0.2
,则最后形成的hash join为left_key_expr=[#0.1, #0.2], right_key_expr=[#1.2, #1.1]
。
然后,在OptimizeNLJAsHashJoin
函数主体中,我们只需遍历语法树的所有结点,然后对其进行判定,符合条件则将其转化为hash join即可。
++You will finally implement a few more common executors, completing your implementation in the following files:
++
+- +
src/execution/sort_executor.cpp
- +
src/execution/limit_executor.cpp
- +
src/execution/topn_executor.cpp
- +
src/optimizer/sort_limit_as_topn.cpp
You must implement the
+IndexScanExecutor
in Task #1 before starting this task. If there is an index over a table, the query processing layer will automatically pick it for sorting. In other cases, you will need a special sort executor to do this.For all order by clauses, we assume every sort key will only appear once. You do not need to worry about ties in sorting.
+
++If a query’s
+ORDER BY
attributes don’t match the keys of an index, BusTub will produce aSortPlanNode
for queries such as:+ +
EXPLAIN SELECT * FROM __mock_table_1 ORDER BY colA ASC, colB DESC;如果要求排序的key不是index key,就会用到这个sort executor。
+This plan node has the same output scheme as its input schema. You can extract sort keys from
+order_bys
, and then usestd::sort
with a custom comparator to sort the child node’s tuples. You may assume that all entries in a table will fit entirely in memory.If the query does not include a sort direction (i.e.,
+ASC
,DESC
), then the sort mode will bedefault
(which isASC
).
++我有一个类Tuple,另一个类Executor。我想实现一个Tuple的比较函数,但需要用到类Executor的成员变量,那么我该怎么写一个可以用于std::sort的cmp函数
+
最终给出的提示是这样的,实现一个函数对象。
+struct CompareTuplesByOrder { |
可以看到,其本质是通过重载”()”运算符来实现的,感觉是一个很有意思的trick。
+它提示的实现思路很简单,就是大概从sort plan node获取所有key,然后用std:sort
即可,默认升序,并且所有entry都是in-memory的。
有一点值得注意的是,在sql语言中,排序是可以指定多个关键词+不同顺序(关键词出现顺序表明排序优先级)的,如order by col1 ASC, col3 DESC
。所以我们需要在comparator实现中按照优先级(也即order_by_
数组顺序)一步步比较。
比较难的地方大概还是c++知识,也即如何为std:sort
实现一个较为复杂的comparator。具体操作可见一些想法-comparator实现
。
++The
+LimitPlanNode
specifies the number of tuples that query will generate. Consider the following example:+ +
EXPLAIN SELECT * FROM __mock_table_1 LIMIT 10;The
+LimitExecutor
constrains the number of output tuples from its child executor. If the number of tuples produced by its child executor is less than the limit specified in the plan node, this executor has no effect and yields all of the tuples that it receives.This plan node has the same output scheme as its input schema. You do not need to support offsets.
+
挺简单的,就是限制输出的数量,没什么好说的。
+++Finally, you should modify BusTub’s optimizer to efficiently support top-N queries. (These were called top-K queries in class.) Consider the following:
++ +
EXPLAIN SELECT * FROM __mock_table_1 ORDER BY colA LIMIT 10;By default, BusTub will plan this query as a
+SortPlanNode
followed by aLimitPlanNode
. This is inefficient because a heap can be used to keep track of the smallest 10 elements far more efficiently than sorting the entire table.Implement the
+TopNExecutor
and modify the optimizer to use it for queries containingORDER BY
andLIMIT
clauses.An example of the optimized plan of this query:
++ +
TopN { n=10, order_bys=[(Default, #0.0)]} | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)
MockScan { table=__mock_table_1 } | (__mock_table_1.colA:INTEGER, __mock_table_1.colB:INTEGER)Hint: See
+OptimizeSortLimitAsTopN
for more information, and check the Optimizer Rule Implementation Guide for details on implementing an optimizer rule.Note: At this point, your implementation should pass SQLLogicTests #16 to #19. Integration-test-2 requires you to use release mode to run.
+
感觉标准做法应该是使用快速排序的partion思想。但是这里的话,我打算使用另一个实现更简单的思路,也即维护一个元素个数为limit_
的有序set,每次插入元素同其最小值比较即可,这样的时间复杂度为O(nlogk)
。当k不会太大的时候,我觉得这样应该还是会比快排要快些的。
本来写到这准备开开心心提交了,突然发现自己的版本似乎跟仓库最新不大一样。rebase了感觉一下午(最后甚至还找了个以前写的小bug……),最后才终于提交完获得了full score……
+bustub仓库中的每个课程版本都是有这样的小tag了,一开始没发现直接大力出奇迹rebase最新,结果整了半天人麻了。。。
+TODO
+]]>参考
+CMU 15-445 Project 2 (Spring 2023) | 关于 B+Tree 的十个问题
+ + +++In this programming project you will implement a B+Tree index in your database system.
+Your implementation will support thread-safe search, insertion, deletion (including splitting and merging nodes包括分裂和合并结点), and an iterator to support in-order leaf scans.
+
它这里的B+树(以及wiki里的)跟王道考研讲得不大一样。王道考研的B+每个结点一个关键字对应一个child,但是这里的是B树的形式。
+++You must implement three Page classes to store the data of your B+Tree:
++
+- +
B+Tree Page
+BPlusTreePage
下面那两个的基类
+- +
B+Tree Internal Page
+An Internal Page stores m ordered keys and m+1 child pointers (as page_ids) to other B+Tree Pages.These keys and pointers are internally represented as an array of key/page_id pairs.
+Because the number of pointers does not equal the number of keys, the first key is set to be invalid, and lookups should always start with the second key.
+At any time, each internal page should be at least half full.【min_size<= <=max_size】
+During deletion, two half-full pages can be merged, or keys and pointers can be redistributed to avoid merging. During insertion, one full page can be split into two, or keys and pointers can be redistributed to avoid splitting.
+- +
B+Tree Leaf Page
+The Leaf Page stores m ordered keys and their m corresponding values. In your implementation, the value should always be the 64-bit record_id for where the actual tuples are stored; see the
+RID
class, insrc/include/common/rid.h
.*Note:* Even though Leaf Pages and Internal Pages contain the same type of key, they may have different value types. Thus, the
+max_size
can be different.
大概就是有一个基类结点,它有两个子类,一个表示b+树的leaf node,另一个表示b+树的internal node,每个结点都占据一个内存页。
+也就是说,一个内存页中存储着一个结点类对象。每次我们都是读取一页到内存中,然后将它类型转换为TreeNodePage*,就可以访问其里面的存储数据的数组array_
了。体会一下这个思想。
值得一提的是,LeafPage
的成员变量中有个这样的成员:
private: |
它就是柔性数组成员。
+++在C++中,Flexible Array Member(柔性数组成员)是一种用于定义具有可变大小的结构的技术。它通常用于在结构的末尾声明一个数组,该数组的大小是动态确定的,这允许你在使用该结构时更灵活地处理变长数据。
+在你提供的代码片段中,
+MappingType array_[0];
是一个柔性数组成员的例子。这里array_
后面有[0]
,这并不表示它们的大小是固定的0。相反,它们的大小是在运行时动态确定的,而[0]
的写法是一种历史上的技巧,用于告诉编译器这是柔性数组成员。例如,如果有一个结构定义如下:
++ +
struct MyStruct {
// 其他成员...
MappingType array_[0];
};你可以根据需要为
+array_
分配任意数量的内存,例如:+ +
int arraySize = 10; // 你想要的数组大小
MyStruct* myObject = static_cast<MyStruct*>(operator new(sizeof(MyStruct) + arraySize * sizeof(MappingType)));
// 在这里你可以使用 myObject,并通过 myObject->array_ 访问柔性数组
// 记得在使用完毕后释放内存
operator delete(myObject);在这个例子中,
+array_
可以用于存储可变大小的数据,而结构体MyStruct
的大小将动态地调整为sizeof(MyStruct) + arraySize * sizeof(MappingType)
。这样的设计通常在需要处理变长数据块的场景中比较有用。请注意,在C++17之后,你也可以使用std::byte
类型来定义柔性数组成员。
拥有柔性数组成员的实例需要动态分配内存(或者像接下来的把一块内存空间interpret一下),柔性数组成员会占用其他成员没有占用的剩下的空间,也即:
++--------------------------+ |
++The index should support only unique keys; if you try to reinsert an existing key into the index, it should not perform the insertion, and should return false. key必须unique
+B+Tree pages should be split (or keys should be redistributed) if an insertion would violate the B+Tree’s invariants. 插入时需要分裂
+If an insertion changes the page ID of the root, you must update the
+root_page_id
in the B+Tree index’s header page. You can do this by accessing theheader_page_id_
page, which is given to you in the constructor. Then, by usingreinterpret_cast
, you can interpret this page as aBPlusTreeHeaderPage
(fromsrc/include/storage/page/b_plus_tree_header_page.h
) and update the root page ID from there. You also must implementGetRootPageId
, which currently returns 0 by default.对root_page_id
的一切访问,都需要通过header_page_id_
。如果插入后改变了root的page ID,需要更新root_page_id
。We recommend that you use the page guard classes from Project 1 to help prevent synchronization problems. For this checkpoint, we recommend that you use
+FetchPageBasic
(defined insrc/include/storage/page/
) when you access a page. 在当前task中,我们推荐你使用pro1实现的page guard,比如说这里如果要访问一页,就需要用FetchPageBasic
。You may optionally use the
+Context
class (defined insrc/include/storage/index/b_plus_tree.h
) to track the pages that you’ve read or written (via theread_set_
andwrite_set_
fields) or to store other metadata that you need to pass into other functions recursively.你可以随意使用和修改Context
class,它大概就是一个存储共享信息的对象。If you are using the
+Context
class, here are some tips:如果你要用,要注意以下几点:+
+- +
You might only need to use
+write_set_
when inserting or deleting. 当你在为B+树插入/删除结点时,需要用到write_set_
。【为什么?这个set存储的是修改路径上的结点吗?然后如果要分裂/合并结点,只需什么while(pop且需要分裂/合并){分裂/合并}??所以说这里的deque是栈结构?】也就是说,其实我们就可以不用递归了,而是将上下文存储在context->write_set_这个栈里面就行了?大概是这个意思吧
+It is possible that you don’t need to use
+read_set_
, depending on your implementation.read可以用递归(比较简单)也可以不用,所以说具体看实现。
+- +
You might want to store the root page id in the context and acquire write guard of header page when modifying the B+Tree.你需要将root page id存储在context,并且在修改b+树(插入、删除)时获取header page的WritePageGurad。
+- +
To find a parent to the current node, look at the back of
+write_set_
. It should contain all nodes along the access path.如果想要寻找当前node的父亲,可以看看write_set_.back
,它包含了访问路径上所有结点的引用【所以确实是当成栈来用了】- +
You should use
+BUSTUB_ASSERT
to help you find inconsistent data in your implementation. 需要使用BUSTUB_ASSERT
。For example, if you want to split a node (except root), you might want to ensure there is still at least one node in the
+write_set_
. If you need to split root, you might want to check ifheader_page_
isstd::nullopt
.如果你想要分割一个根节点以外的node,那你必须保证
+write_set_
中至少有一个结点;如果你想要分割根节点,那你必须保证header_page_
非空。- +
To unlock the header page, simply set
+header_page_
tostd::nullopt
. To unlock other pages, pop from thewrite_set_
and drop.如果你想要不锁住header page,那就置其为空指针;如果想释放别的页,那就将它从write_set_
pop出来就行。【这是因为我们要用到的page类型都是page guard,可以析构时unpin吗?】
由于各种原因,lab2的战线还是拉得太长了。四月份完成了代码初版,中间修了几个bug勉强通过了insertion test,然后一直到十一月底的现在才再次捡起来。不得不说,回看当初的代码,还是能够很清晰地感受到自己这半年多来的成长的,令人感慨。
+我先是花了一天的时间重构了下以前写的所有代码,然后再花了两天时间修bug终于通过了insertion test和sequence scale test,并且将b+树的代码修到了我满意的地步(指不像以前那样一坨重复代码和中文注释。。。)。
+这里简要介绍下B+树的插入实现及我觉得实现中需要注意的几个要点吧。
+B+树的插入流程大概是这样的:
+查找到key要插入的叶子结点(途中需要维护write_set,也即查找路径)
+判断结点是否满
+未满,直接插入即可。(我采取插入排序的方法)
+已满,需要对结点进行分裂。
+推举出中间结点tmp_key,它和新结点page_id接下来将插入到父节点中。
+持续进行分裂:
+需要注意具体的分裂方法,我认为其中internal page size == 3的情况尤为棘手。在具体实现中,我是这样分裂的:
+推举出将要被插入到父节点的tmp_key
+该推举出的key将不会出现在分裂后的新旧结点中,而是会被加入到父节点中。默认为(m + 1) / 2
【m为max size】。
但是要尤其注意size为3的case,此时tmp_key为array_[2]
,很有可能右边结点为空。所以我们需要做点特殊处理:
insert_key > array_[(m + 1) / 2]
时,我们推举(m + 1) / 2
这个结点。insert_key < array_[m / 2]
,我们转而推举m / 2
(此时为array_[1]
)。insert_key < array_[(m + 1) / 2]
且insert_key > array_[m / 2]
时,我们应该对此做出特殊处理,推举insert_key。在此为了代码实现方便,我们还需要调换insert_key和tmp_key的地位。special = false; |
分裂旧结点
+被推举出的tmp_key的value及其右部元素会变成新结点,左部依然留在旧结点,tmp_key会到父节点中去。也即如下图所示:
+![未命名文件 (1)](./cmu15445/未命名文件 (1).png)
+依然是注意上面那个case3特殊情况,需要交换insert key和middle key:
+if (!special) |
持续进行推举和分裂,直到父节点不用分裂
+此时直接将insert key和insert value插入排序到父节点即可。
+然后是Iterator的话,我感觉这也是设计得很不错,让我们亲手写了下c++的重载运算符,也是让我学到了很多c++知识。。。
+感觉问题其实不多,主要还是debug有点痛苦花了很长时间()
+切换内核前后报错。
+Check for working C compiler: /usr/bin/cc - broken
+感觉可能是内核切来切去,导致cmake cache发生了点小问题?总之我最后在5.11内核把build文件删了,重新执行cmake -DCMAKE_CXX_COMPILER=$(which g++) -DCMAKE_C_COMPILER=$(which gcc) ..
就ok了。
我发现在这里创建的root最后好像会被释放掉?
+比如我看到新root的page为6,连接也做得好好的,最后出了函数就寄了:
+还有一个是发现新的leaf page好像不大对,其类型甚至是internal呃呃,我调下看看
+尼玛,绷不住了是这里:
+原来写的
+改了之后test2马上ok,乐
+还弄了个commit修:
+auto INDEXITERATOR_TYPE::operator*() -> const MappingType & |
这个函数卡了我还挺久。。。里面逻辑很简单,不过难就难在怎么构造出一个const MappingType &
。
如果这样:
+INDEX_TEMPLATE_ARGUMENTS |
会说你临时对象不能作为引用。如果这样:
+INDEX_TEMPLATE_ARGUMENTS |
又会找不到机会delete导致内存泄漏。冥思苦想了半天不知道该怎么办,最后从网上看了别人怎么写的:
+INDEX_TEMPLATE_ARGUMENTS |
我服了。
+不过可能有更好的解决方法?可惜我c++水平不大够,所以暂时想不出来了。
+由于有了insert的沉淀,remove的实现便相较不大困难了,写完代码到通过内置的delete测试只花了一天的时间。
+找到需要操作的叶结点路径
+判断叶子结点属于以下四种策略中的哪一种,执行对应策略(优先级从高到低):
+直接删除
+当删除后叶结点元素数仍在合法范围,并且路径上父节点没有target key,直接删除然后返回即可。
+更新父节点路径
+当删除后叶结点元素数仍在合法范围,并且路径上父节点有target key,直接删除然后向上回溯更新父节点即可。
+窃取兄弟元素
+If do a steal, we should update related key in the parent, and update up till reaching the root. |
当删除后叶结点元素数过少,并且左右兄弟元素充足,则从左右兄弟窃取一个。优先窃取元素最多者。
+窃取左兄弟
+窃取左兄弟的最大元素
+需递归更新自身父节点路径上的对应值。
+窃取右兄弟
+窃取右兄弟的最小元素
+需要递归更新自身和右兄弟父节点路径上的对应值。
+之后返回即可。
+合并
+/* |
当删除后叶结点元素数过少,并且左右兄弟元素也都是最小值,那么需要与左右兄弟之一进行合并。优先合并左兄弟。合并都为大->小,也即target->左兄弟 或者 右兄弟->target。
+需要递归删除父节点路径上的merge from元素。
+可以看到,1/2/3三种情况都可以实现简单地直接返回。4稍显复杂,由于递归删除,所以需要对每一个父节点都再次进行上面几种策略的判断,直到遇到情况123返回为止。
+一个比较sb的小bug……
+这位可更是重量级,足足花了我三天的时间……不过感觉第一次处理这么一个复杂的并发情景,花的时间还是值得的。
+最后的结果虽然很一般(指排行榜倒数水平。。。),但至少还是过了。就先这样吧。
+我实现了crabbing lock+optimal lock。对于Insert和Remove,都是先在一开始获取header page和路径上父节点的读锁,然后在之后有可能向上更新时(比如说Insert的需要分裂、Remove的Update和Merge两种情况),丢弃所有读锁,然后获取header page和路径上父节点的写锁。
+不过感觉我这个思路还是略有粗糙,因为相当一部分时间都得占用header page的写锁。但是我思考了一下细粒度方案,发现还是有点难实现。比如说,对于insert,细粒度化的方式也许就是一直持有header page的读锁,一直到需要分裂根节点时,才释放读锁获取写锁。但这样一来就会暴露一个危险的空窗期(而且感觉这个空窗期还不小),当你真的拿到写锁,这树的结构可能已经变得不知道什么样了。在这种情况下,你就需要再做一次回溯工作,也即获取从新root结点到旧root结点的路径,递归插入insert key和insert value,最后安全分裂根节点(因为此时已经安全持有了header page写锁)。感觉思路也是比较易懂,但是实现上还是太麻烦了,所以先暂且搁置吧。
+这种感觉大多还是在面向测试用例见招拆招……所以其实感觉没什么好说的。
+这个并发问题是这样的,我原来是先evict,然后再写回被替换的页面,写回过程中磁盘没加bpm锁。这就会出现这样一个情况:
+一个page被进程A evict,进程A还没执行写回的时候这个page又被进程B捡回来了,因为还没写入所以磁盘空空如也。这时候pages_latch_这个细粒度锁不能防范这种情况,是因为此时这个page对应的container不是同一个,所以fid不同,细粒度锁不同导致寄。
+解决方法是要么写的时候持有bpm锁,但是这太太慢了。另一个就是干脆直接在unpin的时候不带bpm锁顺便写回了。也即把写回从evict后移到unpin中立即写回:
+if (pages_[fid].GetPinCount() == 0 && pages_[fid].IsDirty()) { |
+ + ++
看起来感觉大多性能损耗还是在bpm上,特别是LRU-K。也许是我的全局锁太暴力了。
+-+本次实验一直在强调的一点就是,TCP的功能是将底层的零散数据包,拼接成一个reliable in-order的byte stream。这个对我来说非常“振聋发聩”(夸张了233),以前只是背诵地知道TCP的可靠性,这次我算是第一次知道了所谓“可靠”究竟可靠在哪:一是保证了序列有序性,二是保证了数据不丢失(从软件层面)。
-还有一个就是大致了解了cs144的主题:实现TCP协议。也就是说,运输层下面的那些层是不用管的吗?不过这样也挺恰好,我正好在学校的实验做过对下面这些层的实现了,就差一个TCP23333这样一来,我的协议栈就可以完整了。
-
可重定位的代码通过linker和loader重定位这部分内容就是在之前那本书学过的。
+从中,我们也可以看到有语法分析、中间代码的影子。
+词法分析相当于通过DFA NFA捉出各类符号,形成简单的符号表和token list;语法分析相当于对token list组词成句,判断该句子是否符合语言规则;语义分析相当于对词句进行类型判断和中间代码的生成,获得基本语义。
+语法制导翻译:语义分析和中间代码生成集成到语法分析中
+将结果转化为token的形式。
+从token list中识别出各个短语,并且构造语法分析树。
+相当于是通过文法来进行归约(自底向上的语法分析),从而判断给定句子是否合法。
+种属就是比如是函数还是数组之类的。
+静态绑定
+包括绑定代码相对地址(子程序)、数据相对地址(变量)
+波兰也就是前序遍历二叉树(中左右),逆波兰也就是后序遍历二叉树(左右中)
+无关机器
+有关机器
+这也挺好理解,相当于管理符号表吧。
+了解了编译程序的基本结构,那么我们就可以想想该怎么实现这个编译器了。
+最直观的想法是,我们有几个步骤就对代码进行多少次扫描:
+也就是说:
+帅的。
+这大概是描述了我们到时候会怎么实现这两个阶段代码。
+不过确实,词法分析可以看作是正则匹配,语法分析可以看作是产生式。
+字母表
+串
+克林闭包中的每一个元素都称为是字母表Σ上的一个串
+如果文法用于描述单词,基本符号就是字母;用于描述句子,基本符号就是单词
+文法的形式化定义
+由于可以从它们推出其他语法成分,故而称之为非终结符
+还真是最大的语法成分
+产生式
+符号约定
+文法符号串应该就是指既包含终结符也包含非终结符的,也可能是空串的串。
+注意终结符号串也包括空串。
+这部分就是要讲怎么看一个串是否满足文法规则,那么我们就需要先从什么样的串是满足文法规则的串开始说起,也即引入“语言”的概念。
+推导与归约
+然后也分为最左推导和最右推导,对应最右归约和最左归约。
+故而,如果从开始符号可以推导(派生)出该句子,或者从该句子可以归约到开始符号,那么该句子就是该语言的句子。
+句子与句型
+句型就是可以有非终结符,句子就是只能有终结符
+语言
+文法解决了无穷语言的有穷表示问题。
+emm,就是好像没有∩运算
+有正则那味了
+0型
+1型
+之所以是上下文有关,是因为只有A的上下文为a1和a2时才能替换为β【666666,第一次懂】
+CSG不包含空产生式。
+2型
+左部只能是一个非终结符。
+3型
+产生式右部最多只有一个非终结符,且要在同一侧
+看起来还能转(是的,自动机教的已经全忘了())
+正则文法用于判定大多数标识,但是无法判断句子构造。
+也就是说,每个句型都有自己对应的分析树。那么接下来就介绍什么是句型的短语
+意思就是直接短语是高度为2的子树的边缘,直接短语一定是某个产生式的右部,但是产生式右部不一定是给定句型的直接短语(因为有可能给定句型的推导用不到那个产生式)
+通过自定义规则消除歧义
+最后两条值得注意
+所以真正的终止是输入带到末尾并且指向终态
+66666,还能这么捏起来
+关键字是在识别完标识符之后进行查表识别的
+说实话没太看懂
+根据给定文法,识别各类短语,构造分析树。所以关键就是怎么构建分析树
+可以看做是推导(派生)的过程。
如果同一非终结符的各个产生式的可选集互不相交,就可以进行确定的自顶向下分析:
这两个分析也是我们的分析方法需要解决的。
+也就是说,在自顶向下分析时,采用的是最左推导;在自底向上分析时,最左归约和最右推导才是正道!
+大概流程应该是,有产生式就展开,然后当产生式右部有多个候选式的时候再根据输入决定。
+如果有多个以输入终结符打头的右部候选,那就需要逐一尝试错了再回溯,因而效率较低。
+66666,这其实就可以类似于动态规划了吧
+【感觉这里也能窥见一些算法设计的思想。
+仔细想想,我们在引入动态规划时,也是这个说辞:对于一些回溯问题,回溯效率太低,所以我们就可以提前通过动态规划的思想构造一个状态转移表,到时候只需从零开始按照表进行状态转移即可。
+仔细想想,这不就是这里这个预测分析提出的思想吗!真的牛逼,6666
+我记得KMP算法一开始也是这个思想,感觉十分神奇】
+这个左递归及其消除方法解释得很形象
+先转化为直接左递归
+666666这个解读可以,感觉这个就跟:
+这个“向前看”有异曲同工之妙了。
+LL(1)文法才能使用预测分析技术。判断是否是LL文法就得看具有相同左部的产生式的select集是否相交。
+S文法不包含空产生式
+也就是说,B的Follow集为{b,c},只有当输入符号为b/c时才能使用空产生式。
+first集和follow集不交。
+这下总算知道这两个是什么玩意了。也就是这样:
+输入符号与B的First集元素匹配
+直接用那个产生式
+否则,看输入符号是否与Follow集元素匹配
+是
+若B无空产生式,报错;否则,使用B的空产生式(相当于消了一个符号但不变输入带指针)
+否
+报错
+这个感觉跟first集有点像,相当于是右部只能以终结符开始的形式,所以下面的LL文法会增强定义。
+当该非终结符对应的所有SELECT集不相交,就可以进行确定的自顶向下语法分析。这个思想也将贯穿下面的LL文法
+最后,如果同一非终结符的各个产生式的可选集互不相交,就可以进行确定的自顶向下分析:
+这几个推理下来,真是让人感觉酣畅淋漓!
+确定的自顶向下分析的核心就是,给定一个当前所处的非终结符和一个输入字符[E, a],我们可以唯一确定一个产生式P用于构建语法分析树。
+也即,同一个非终结符的所有产生式的SELECT集必须是不交的【才能确保选择产生式的唯一性】。因而,问题就转化为了如何让SELECT集不交。
+我们需要对空产生式和正常产生式的SELECT集计算做一个分类讨论。
+空产生式
+由于可以推导出空,相当于把该符号啥了去读下一个符号,因此我们的问题就转化为输入字符a是否能够跟该符号后面紧跟着的字符相匹配。而紧跟着的字符集我们将其成为FOLLOW集,如果a在follow集中,那么就可以接受,否则不行。
+对于LL(1)文法,相当于是进一步处理了简介推出空的串:
+ 由于α串->*空,则α串必定仅由非终结符构成。那么它能推导出的所有可能即为SELECT集。故而为First(α)∪Follow(α)
非空产生式
+很简单,就是其First集。
+故而,只需要让这些计算出来的First集合不交,就能进行确定的自顶向下语法分析,构造确定的语法分析树。不得不说真的牛逼。
+感觉其“预测分析”的“预测”主要体现在对空产生式的处理上。
+总算懂了为什么LL(1)能够解决这个回溯效率太低的问题了,太牛逼。不过问题是怎么转化为LL(1)呢()上面的消除回溯和左递归只是一部分而已吧。
+这个消除二义性是啥玩意?二轮的时候看看PPT怎么讲的
+66666,它这个计算follow集的方法就很直观
+declistn有个空产生式,那么我们看得看②,而②的declistn排在最后,也就是说declistn的follow集就是其左部declist的follow集【6666】,所以我们看①,可以发现declist后面为:。
+如果是终结符,就直接==比较;非终结符,就把token传入到其对应的过程。
+66666
+感觉从中又能窥见动态规划的同样思想了。下推自动机其实感觉就像是递归思想(或者说顺序模拟递归,因为它甚至有一个栈,出栈相当于达成条件递归return),动态规划的话可能有点像是把每个不同状态以及不同状态时的栈顶元素整成一个2x2的表,所以感觉思想类似。
+注意,是栈顶跟输入一样都是非终结符才会移动指针和出栈
+值得注意的是,输出的产生式序列就对应了一个最左推导。
+其实也挺有道理,栈顶是非终结符,但是输入是它的follow集,那我们自然而然可以想到把这b赶跑,看看下面有没有真的它的follow集在嗷嗷待哺。
+正确识别句柄是一个关键问题。
+句柄:当前句型的最左直接短语。【最左、子树高度为2】
+每次句柄形成就将它归约,因而保证一直是最左归约(recall that,句柄一定是某个产生式的右部,并且每次最左句柄一旦形成就归约)
+正如上面的LL分析,每次推导要选择哪个产生式是一个问题;这里的LR分析,每次归约要选择哪个产生式,也即正确识别句柄,也是一个关键问题。
+所以,我们应该把句柄定义为当前句型的最左直接短语。
+如下图所示,左下角是当前句型(画红线部分)的语法分析树,红字为在栈中的部分,蓝字为输入符号串剩余部分。当前句型的直接短语(相当于根节点的高度为二的子树,或者说子树前两层)有两个,一个是以<IDS>
为根节点的<IDS> , iB
,另一个是<T>
为根节点的real
。
而LR分析技术的核心就是正确地识别了句柄。
+也就是说LR技术就是用来识别句柄的,识别完了句柄就可以构建类似自顶向下的预测分析那样的自动机表来进行转移。
+移进状态
+·后为终结符
+待约状态
+·后为非终结符
+归约状态
+·后为空
+以前感觉一直很难理解GOTO表的作用,现在感觉稍微明白了点了,你想想,归约之后的那个结果是不是有可能是另一个产生式的右部成分之一,也即一个新的句柄?并且这个也是由你栈顶刚归约好的那个左部和下面的输入符号决定的。那么你自然而然需要切换一下当前状态,以便之后遇到那个产生式的时候能发现到了。
+那么,剩下的问题就是如何构造LR分析表了:
+也就是它会整一个终结符之间的优先级关系。。。
+也就是说:
+a=b
+相邻
+a<b
+也即在A->aB时,b在FIRSTOP(B)中(理解一下,这个First指在前面。。。)
+a>b
+也即在A->Bb时,a在LASTOP(B)中(理解一下,这个LAST指在后面。。。)
+我服了
+好像#这个固定都是,横的为左,竖的为右
+根据优先关系来判断移入和归约
+每个分析方法其实都对应着一种构造LR分析表的方法。
LR(0)通过构造规范LR0项集族,从而构造LR分析表,从而构造LR0 DFA来最终进行语法分析。
每一个项目都对应着句柄识别的一个状态。
+而肯定不可能整那么多个状态,所以我们需要进行状态合并。(这样也就很容易理解LR的状态族构建了。)
+它这里也很直观解释了为什么点遇到非终结符就需要加入其对应的所有产生式,因为在等待该非终结符就相当于在等待它的对应产生式的第一个字母。
+上面这东西就是这个所谓的规范LR(0)项集族了。
+但是会产生移进归约冲突:
+还有归约归约冲突:
+所以我们就把没有冲突的叫LR(0)文法。
+感觉上述两个问题都是因为有公共前缀【包括空产生式勉强也能算是这个情况】,导致信息不足无法判断应该怎么做,多读入一个字符(也即LR(1))应该可以有效解决该问题。
+其实本质还是识别句柄问题,也即此时是归约还是移入,得看是不是句柄。故而LR0信息已经不能帮我们识别句柄了。
+Follow集可以帮助我们判断。由该状态I2可知,输入一个*应该跳转到I7。如果在I2把T归约为一个E,由Follow集可知E后面不可能有一个*,也就说明在这里进行归约是错误的,应该进行移入。
+这种依靠Follow集和下一个符号判断的思想,就会运用在SLR分析中。
+但值得注意的是SLR分析的条件还是相对更严苛,它要求移进项目和归约项目的Follow集不相交,所以它也会产生像下图这样的冲突:
+SLR将子集扩大到了全集,显然进行了概念扩大。
+含义为只有当下一个输入符号是XX时,才能运用这个产生式归约。这个XX是产生式左部非终结符的Follow子集。
+这玩意只有归约时会用到,这个很显然,毕竟前面提到的LR0的问题就是归约冲突。
+对了,值得注意的是这个FIRST(βa)
,它表示的并不是FIRST(a)∪FIRST(β)
,里面的βa应该取连接意,也即,当β为非空时这玩意等于FIRST(β)
,当β空时这玩意等于FIRST(a)
。
刚刚老师对着这个状态转移图进行了一番强大的看图写话操作,我感觉还是十分地牛逼。她从这个图触发,讲述了状态I2为什么不能对R->L进行归约。
+假如我们进行了归约,那么我们就需要弹出状态I2回到I0,压入符号R,I0遇到符号R进入了I3,I3继续归约回到I0,I0遇到符号S到状态I1,但1是接收状态,下一个符号是=不是$,所以错了。
+比如说I8和I10就是同心的。左边的那个实际上是LR0项目集,所以这里的心指的是LR0。
+然而,LR(1)会导致状态急剧膨胀,影响效率,所以又提出了个LALR分析。
+跟前面的SLR对比可以发现,相当于它就是多了个逗号后面的条件。但是这是可以瞎合的吗?不会出啥问题不。。。
+好吧问题这就来了,LALR可能会产生归约归约冲突。但值得注意的是,它不可能出现归约移入冲突,因为LR1没有这个东西,而LALR只是修改右边的符号,所以也不会有这个。
+因为LALR实际上是合并了展望符集合,这东西与移进没有关系,所以只会影响归约,不会影响移进。
+LALR可能会产生归约归约冲突。但值得注意的是,它不可能出现归约移入冲突,因为LR1没有这个东西,而LALR只是修改右边的符号,所以也不会有这个。
+它有可能做多余的归约动作,从而推迟错误的发现。
+形式上与LR1相同;大小上与LR0/SLR相当;分析能力介于SLR和LR1之间;展望集仍为Follow集的子集。
+感觉一路看下来,思路还是很流畅的。LR0会产生归约移进冲突和归约归约冲突,所以我们在归约时根据下一个符号是在移进符号还是在Follow集中来判断是要归约还是要移进。但是SLR条件严苛,对于那些移进符号集和Follow集有交的不适用,并且这种情况其实很普遍。加之,出于这个motivation:其实不应该用整个Follow集判断,而是应该用其真子集,所以我们开发出来个LR1文法。然后LR1文法虽然效果好但是状态太多了,所以我们再次折中一下,造出来个效果没有那么好但是状态少的LALR文法。
+所以我们可以用LR对二义性文法进行分析
+我们可以通过自定义规则来消除二义性文法的归约移入冲突
+对于状态7,此时输入+ or *会面临归约移入冲突。由于有E->E+E归约式子,可以知道此时栈中为E+E。当输入*,由于*运算优先级更高,所以我们在此时进行移入动作转移到I5;当输入+,由于同运算先执行左结合,所以我们此时可以安全归约。
+对于状态8,由于*运算比+优先级高,且左结合,所以始终进行归约。
+它这个意思大概就是,符号栈和状态栈都一直pop,直到pop到一个状态,GOTO[符号栈顶,状态栈顶]有值【注意,始终保持符号栈元素+1 == 状态栈元素数+1
】。然后,一直不断丢弃输入符号,直到输入符号在A的Follow集中。此时,就将GOTO值压入栈中继续分析。
【这其实也很有道理。如果输入符号在A的Follow集,说明A之后很有可能可以消耗这个输入符号。】
+注意:
+思想:
+也可能是先入为主吧,感觉用实验的方法来理解语义分析比较便利。语义分析相当于定义一连串事件,附加在每个产生式上。当该产生式进行归约的时候,就执行对应的语义事件。而由于执行语义分析时需要的符号在语法分析栈中,所以我们也同样需要维护一个语义分析栈,在移进时也需要进栈。
+语义分析一般与语法分析一同实现,这一技术成为语法制导翻译。
+可以回忆一下实验,相当于对每个产生式进行一个switch-case,然后依照产生式的类别和代码规则进行出栈入栈来计算属性值。
+一个很简单区分综合属性和继承属性的方法,就是如果定义的是产生式左部的属性,那就是综合属性;右部,那就是继承属性
+这个东西就是我们实验里写的,副作用也是更新符号表。
+没有副作用的SDD称为属性文法。
+而感觉语法分析这个过程的产生式归约顺序就能一定程度上表示了这个求值顺序
+蛤?这不是你自己规则设计有问题吗,关我屁事
+其实我还是不大理解,因为这个规则不是user定义的吗?所以产生环不也是它的事,难道说自顶向下或者自底向上分析还能优化SDD定义??
+感觉它意思应该是这样的,有一个方法能绝对不产生循环依赖环,也即将自底向上/自顶向下语法分析与语义分析结合的这个方法。这个方法就是它说的真子集。
+所以我们接下来要研究的就是什么样的语义分析可以用自顶向下or自底向上语法分析一起制导。
+那确实,你自底向上想要计算继承属性好像也不大可能
+对应了自顶向下的最左推导顺序
+S-SDD包含于L-SDD
+当归约发生时执行对应的语义动作
+还需要加个属性栈
+所以S-SDD+自底向上其实很简单,因为只需在归约的时候进行语义分析,在移进的时候push进属性栈就行了。
+具体的S-SDD结合语法分析的分析过程可以看视频。
+这个例子还算简单的,毕竟只是综合属性的计算而已,只需要加个属性栈,保存值就行了。
+我们可以来关注一下这个SDT的设计,也很简单。可以产生式和语义规则分离看待,这也给我们以后设计提供一定的启发。
+这个是自顶向下的语法分析,本来只用一个栈就行了,现在需要进行扩展。T的综合属性存放在它的右边,继承属性存放在它的平行位置。
+当属性值还没计算完时,不能出栈;当综合记录出栈时,它要将属性值借由语义动作复制给特定属性。
+然后语义动作也得一起进栈。
+digit是终结符,只有词法分析器提供值
+此时,digit跟一个语义动作关联,所以我们需要把它的值复制给它关联的这个语义动作{a6},然后才能出栈。
+--本次实验与TCP的关系:
-在我们的
-webget
实现中,正是由于TCP的可靠传输,才能使我们的http request正确地被服务器接收,才能使服务器的response正确地被我们接收打印。而在
-ByteStream
中,我们也做了跟TCP类似的工作:接收substring,并且将它们拼接为in-order的byte stream【由于在内存中/单线程,所以这个工作看起来非常简单】:+
while(is_input_end == false&&pointer<length){
if(buffer.size() == capacity) break;
buffer.push_back(data[pointer]);
pointer++;
}关联的另一个实例:
++
此时由于T’.inh还要被a3用到,所以我们就得在T’出栈前把它的这个inh值复制给a3。
主要是介绍了telnet
指令
用的是telnet带smtp参
-上面的telnet是一个client program。接下来我们要把自己放在server的位置上。
-用的是netcat
指令。
这个确实不难,就是这个地方有点坑:
+当遇到语义动作之后,就执行动作,并且出栈语义动作。
+它这意思应该是遇到每个产生式的每个符号要执行什么动作都是确定的,所以代码实现是可能的。
+可以看到:
+666666666
+感觉这个值得深思,但反正现在的我思不出啥了。。。
+相当于把L-SDD转化为了个S-SDD。具体是这样,把原式子右边的变量替换为marker的继承属性,结果替换为marker的综合属性。那么新符号继承属性怎么算啊。。。不用担心,因为观察可知要使用的这两个非终结符一定已经在栈中了。
+具体分析也看视频就好了。
+false list就是if失败后的那个goto序号,true list是成功的那个goto序号,s.nextline是整个if的下一条指令
+增量生成
+它这个相当于是把符号表和offset都整成了一个栈,毕竟确实过程调用就是得用栈结构的
+之后用到该记录类型,就指向记录符号表即可。
+这个就不用填符号表了,所以helper function都是用来产生中间代码的
+addr属性需要从符号表中获取
+看个乐吧
+在语义动作中实现
+反正意思就是用S.next这个继承属性来表示S.code执行完后的下一个三地址码地址。
+其实不大懂这什么玩意
+抽象
+这两个都是综合属性
+相当于是一个waiting list
+可以理解为,B这个表达式可以分为两种情况,两种情况有一个为真B就为真。那么,B的真回填list相当于也被分为了两种情况,所以要求B的就是把它们合起来。
+原来回填是这个意思
+nextline是一个综合属性
+TODO 这笔之后再看。。。。
+TODO
+静态链也被称作访问链,用于访问存放于其他活动记录中的非局部数据。
+动态链也被称作控制链,用于指向调用者的活动记录。
+反正意思就是既要得到原来的A,又要修改A
+也就是说左边及其所有子树全调完了,才能调下一个兄弟的。
+左边这几点设计规则都十分reasonable,很值得注意。
+不过我其实挺好奇,参数存在那么后面该咋访问。。。。看xv6,似乎是fp指向前面,sp才指向local,也即用了两个栈指针。
+这个控制链也是约定俗成的,具体可以想起来xv6也是类似结构:
+当函数返回的时候,就会进行恢复现场,从而出栈一直到ra,很合理。
+调用序列应该就是设置参数、填写栈帧一类,返回序列就是恢复现场
+传变量、改变meta data、改变top和sp指针
+这段解释了下为什么不用堆,说得很好
+第二点,比如malloc后不free
+每一个嵌套深度的分配一个Display位
+S嵌套深度1,所以占据d[1];Y和X嵌套深度2,所以占据d[2];Z嵌套深度3,所以占据d[3]。
+然后,一开始遇到个S,d1指向S;然后调用Y,d2指向Y;然后Y中调用X,就修改d2指向X;然后调用Z,就修改d3指向Z。
+总之显示栈就是这个变换指针的过程。
+至于控制栈,要打印这里面的display表,就是看层数。如果d1那就打印当前层,d2就打印的12层,d3就123层【不是纯显示栈,是它自己内部的未变换指针的结果】
+结果:SXZ
+静态作用域是空间上就近原则,动态是时间上。
+也就是说这时候非局部的一定是全局变量或者静态的局部变量。
+如果是支持过程声明嵌套,顺着符号表就可以找到其父过程/子过程的数据。
+符号表也可以用于构造访问链,因为过程名也是一种符号。
+不讨论这个
+所以这东西是用来决策寄存器分配的
+反正类似保护现场恢复现场
+在思考自动机和动态规划的关系时,胡乱搜索看到了AC自动机,于是来了解了一下。
--Please note that in HTTP, each line must be ended with “\r\n” (it’s not sufficient to use just “\n” or endl).
+
导致我跟400 Bad Request大眼瞪小眼了好久。。。
-void get_URL(const string &host, const string &path) { |
还有一点值得注意的是,当我这样时:
-TCPSocket sock; |
会报错Operation now in progress
。
--关于socket通信中在connect()遇到的Operation now in progress错误
-遇到此错误是因为将在connect()函数之前将套接字socket设为了非阻塞模式。改为在connect()函数之后设置即可。
-
我觉得这个实验设计得挺好的,写的时候感觉很有意思。我推荐看下 https://github.com/shootfirst/CS144/blob/main/lab-0/apps/webget.cc 里的注释,写得很好很规范,让我明白了很多本来没搞懂的地方,比如说shutdown
的用法。
--实现一个
+ByteStream
类,可以通过read
和write
对其两端进行读写。是单线程程序,因而无需考虑阻塞。考虑一个问题:给出若干个模式串,如何构建一个DFA,接受所有以任一模式串结尾(称为与该模式串匹配)的文本串?
+可以先思考一个更简单的问题:如何构建接受所有模式串的DFA?很明显,**字典树**就可以看做符合要求的自动机。例如,有模式串
+"abab"
、"abc"
、"bca"
、"cc"
,我们把它们插入字典树,可以得到:+
为了使它不仅接受模式串,还接受以模式串结尾的文本串,一个看起来挺正确的改动是,使每个状态接受所有原先不能接受的字符,转移到初始状态(即根节点)。
++
但是如果我们尝试
+"abca"
,我们会发现我们的自动机并不能接受它。稍加观察发现,我们在状态5接受a
应该跳到状态8才对,而不是初始状态。某种意义上来说,状态7是状态5退而求其次的选择,因为状态7在trie上对应的字符串"bc"
是状态5对应的字符串"abc"
的后缀。既然状态5原本不能接受"a"
,我们完全可以退而求其次看看状态7是否可以接受。这看起来很像KMP算法,确实,AC自动机常常被人称作trie上KMP。所以我们给每个状态分配一条fail边,它连向的是该状态对应字符串在trie上存在的最长真后缀所对应的状态。我们令所有状态p接受原来不能接受的字符c,转移到 next(fail(p),c) ,特别地,根节点转移到自己。为什么不需要像KMP算法一样,用一个循环不断进行退而求其次的选择呢?因为如果我们用BFS的方式进行上面的重构,我们可以保证 fail(p) 在p重构前已经重构完成了,类似于动态规划。
++
这样建fail边和重构完成后得到的自动机称为AC自动机(Aho-Corasick Automation)。
+我们发现fail边也形成一棵树,所以其实AC自动机包含两棵树:trie树和fail树。一个重要的性质是,如果当前状态 p 在某个终止状态 s 的fail树的子树上,那么当前文本串就与 s 所对应模式串匹配。
这东西其实是很简单的,但是我还是花了一定的时间,主要原因有两点,一是我不懂c++,所以一些地方错得我很懵逼,二是因为我是sb。
-下面就记录下三个我印象比较深刻的错误吧。
-构造函数我一开始是这么写的:
-结果爆出了这样的错:
-搜了半天也没看懂怎么回事,去求助了下某场外c艹选手,才知道了还有成员变量初始化列表这玩意,这个东西似乎比较高效安全。
-于是我改成了这么写:
-它告诉我buffer
也得初始化。于是我又这么写:
又是奇奇怪怪的错误,说明vector不能这么初始化。
-场外c艹选手看到了这个:
-所以说vector应该这样初始化:
-vector
作为buffer的载体应该使用的是可以从front删除数据的数据结构,比如说deque。【vector也行,但是效率较低】
-具体为什么,可以以数据流为cat为例。执行peek(2)
时,使用vector得到的是at,使用deque得到的是ca。
一开始在write
方法,我是这么写的:
int length = data.length(); |
结果就是测试用例Timeout。我找了很久都不知道错在了哪,最后求助了场外观众【罪过……这次实验太不独立了】,学着他把length改成了这样:
-int length = min(data.length(),capacity-buffer.size()); |
发现成了。
-我去看了看testbench,猜测应该是因为阻塞了,我还以为是deque自身会阻塞【是的,我完全没注意到自己顺手把阻塞写了下去】,查了半天发现不会,最后才发现是自己不小心搞错了呃呃…………
-class ByteStream { |
ByteStream::ByteStream(const size_t cap) : total_write(0),total_read(0),is_input_end(false),capacity(cap),buffer(){ } |
也就是说它的解决方法是加fall边(蓝色)和加新边(红色),
]]>http://localhost/webdemo4_war/*.do
。
学习目的:顺利过考试,以及获取基本的密码学知识,数学原理不重要
+ +只有理论上意义
+实际上不可行
+你也是过渡阶段?
+逆元:
+比如说在G(7)中,2的逆元为4。
+也即,任意整数a,则存在x,a / 2 == a * 4 (mod 7),4为2模7的乘法逆元,记为 2(-1)(mod 7) = 4。
+求逆元的方法是求b^(m-2) mod m。如2^(5) mod 7 = 4。
+确实封闭且结合且单位元且逆元
+确实是环
+有限域就是阶为素数幂的域?
+确实,毕竟系数本身就是域了,除了没定义逆元外别的都满足。
+Nr=Nk的幂数x2
+具体算法详见PPT。
+可以关注下是怎么通过C矩阵求出这个固定多项式的:
+感觉也是类似对明文做的操作
+序列密码的密钥序列是随机的。
+确实,感觉相比上面的这笔就是换了个反馈函数,就达到了2^n-1的周期
+也就是说求最大公因子实际上可以只求共有素数因子
+看起来意思就是公钥完全明文,用的是用户的身份ID;私钥用户自己存着。
+这个角度很有意思,确实是名字一样原理相近,但是目的完全不一样:
+也就是中途会哈希两次吼。
+一样的话就是说明消息没被篡改
+都包含签名算法、验证算法、正确性证明、举例,详细看PPT吧。
+这个有点复杂,可以看看PPT。
+http://localhost/webdemo4_war/*.do
。
也是系统调用socket的参数,了解一下知识多多益善。
domain
-在本次实验中只会取值前两个,即本地通信和IPv4网络通信
-type
-好像比如说取SOCK_DGRAM
就是UDP,取SOCK_STREAM
就是TCP。
/* Socket */ |
上面那俩类其实就是两个包装类,用来将系统调用包装为c++类,看起来很抽象很迷惑。但到这就不一样了!我们开始用上我们之前写的TCP协议的代码了!
-除了跟fd以及socket一致的read
、write
以及close
之外,TCPSocket
最独特的功能,应该就是TCP连接的建立与释放了,其状态转移等逻辑已由我们在Lab0-4实现,此socket类仅实现事件的监听和TCP协议对象生命周期的管理。
在详细说明其两个功能——事件监听和生命周期管理——之前,不妨先了解下其总体的架构。
-TCPSpongeSocket
需要双线程实现。其中一个线程用来招待其owner:它会执行向owner public的connect、read、write等服务。另一个线程用来运行TCPConnection
:它会时刻调用connection的tick方法,并且进行事件监听。
//! \class TCPSpongeSocket |
完成事件监听的核心部分是方法_tcp_loop
以及_initialize_TCP
中对_eventloop
的初始化,还有eventloop
的实现。
看下来其实理解难度不大(虽然细节很多并且我懒得研究了),但我认为很值得学习。
-主要功能是添加我们想监听的事件,有四个,分别是从app得到数据、有要向app发送的数据、从底层协议得到数据、有要向底层协议发送的数据。具体的话,代码和注释都写得很详细就不说了。
-可以看到,TCP与协议栈交互【包括收发数据报】,是通过AdaptT _datagram_adapter;
实现的;TCP与上层APP交互【包括传送数据】,是通过LocalStreamSocket _thread_data;
实现的。
template <typename AdaptT> |
可以看到,_tcp_loop
的功能就是,在condition
为真的时候,一是监听我们之前塞进_event_loop
的所有事件,二是调用TCPConnection
的tick
方法来管理时间。
// condition is a function returning true if loop should continue |
eventloop具体是通过Linux提供的poll机制来进行事件监听的。
-- --怎么说,又一次感受到了“网络就是IO”这个抽象的牛逼之处。操作系统的poll机制和poll函数本质上是针对IO读写来设计的,而正因为网络的本质是IO,正因为网络收发数据包、与上层app交互本质还是IO(因为通过文件描述符),才能在这里采用这种方式进行文件读写。
-我的评价是佩服到五体投地好吧
--
poll函数就是IO等待的一种实现机制。
-- -
int poll(struct pollfd *fds, nfds_t nfds, int timeout);事件类型events可以为下列值:
--
POLLIN:有数据可读
POLLRDNORM:有普通数据可读,等效于POLLIN
POLLRDBAND:有优先数据可读
POLLPRI:有紧迫数据可读
POLLOUT:写数据不会导致阻塞
POLLWRNORM:写普通数据不会导致阻塞
POLLWRBAND:写优先数据不会导致阻塞
POLLMSG:SIGPOLL消息可用
POLLER:指定的文件描述符发生错误
POLLHUP:指定的文件描述符挂起事件
POLLNVAL:无效的请求,打不开指定的文件描述符
我们在前面的eventloop的rule初始化中:
-_eventloop.add_rule(_datagram_adapter, |
这个的意思是针对_datagram_adapter
这个文件的Direction::In
这个事件发生时,就会执行[&]
中的事件。那么Direction::In
是什么?
enum class Direction : short { |
可见,eventloop具体是通过os提供的IO事件机制来进行监听的。
-具体的监听以及执行逻辑由wait_next_event
来实现。它主要干的就是,清理掉那些我们不感兴趣的或者已经似了(比如说对应的fd已经close之类的)的事件,然后找到那些触发到了的active的事件并且调用它们的caller。
具体代码还是有些微复杂的,有兴趣可以去看看,这里就不放了。
-核心部分为方法connect
、listen_and_accept
以及_tcp_main
。
由客户端调用。
-// Client调用 |
负责establish状态的监听以及之后关闭TCP连接的擦屁股工作
-template <typename AdaptT> |
由服务器端调用。
-// Server调用 |
主菜(上面那个)已经说完了,这两个就是简单的包装类,没什么好说的,大概就做了点传参工作,主要差异还是adapter。
-在我们的TCPSpongeSocket
实现中,我们引入了“adapter”的概念。
protected: |
它很完美地以策略模式的形式,凝结出了我们本次实验所需的各种协议栈的共同代码,放进了TCPSpongeSocket
,而将涉及到协议栈差异的部分用adapter完成。
在TCPSpongeSocket
中,adapter主要完成了如下操作:
adapter的tick函数
-// in tcp_loop |
作为订阅事件的IO流
-_eventloop.add_rule(_datagram_adapter, |
TCP层通过对其读写来获取TCP segment
-auto seg = _datagram_adapter.read(); |
记录各类参数
-datagram_adapter.config().destination.to_string() |
具体实现说实话没什么好说的,确实无非也就是上面那几个方法,然后在里面包装下和操作系统提供的tun和tap的接口交互罢了,代码也比较简单,此处就不说了。
-除了对协议栈的实现之外,在app文件夹下还有许多对我们实现的协议栈的应用实例。我认为了解下应用实例也是很重要的。
-其作用就是建立stdin/stdout与socket的关联。它从stdin读输入,作为上层app的输入写入socket;从socket读输出,传给上层app,也即stdout输出。它的具体实现在stdin/stdout之间隔了两条bytestream,分别是_inbound
和_outbound
。
由于stdin、stdout、socket本质上都是fd,所以我们依然可以采用跟上面一样的事件驱动方式。我们只需在socket有输出时马上读给inbound bytestream,在inbound bytestream有输入时马上读给stdout,在stdin有输入时马上写入outbound bytestream,在outbound bytestream有输入时马上读给socket。遵守这4条rule就行了。
-因而,具体实现就是TCPSpongeSocket::_initialize_TCP
和TCPSpongeSocket::_tcp_loop
的结合体,订阅事件+循环等待。由于跟前面类似,在此就不放代码了。
其他都太复杂了,感觉我水平一般还不大能理解,也懒得看了【草】总之先咕咕咕
-]]> -在前两个实验中,我们可以说只是做了点算法上的抽象工作,跟TCP协议还是没什么显著的关系的。但来到了本次实验,一切就都不一样了。
-依然还是这张图。相信此时做过前两个实验之后,看到这张图就会有了不一样的发现。
-我们对TCP协议的实现是由内向外的,先实现里面再实现最外层。前两节实验,我们由内而外实现了ByteStream
和StreamReassembler
;在这次实验中,我们会实现更外层一点的TCPReceiver
。
根据我们前两次实验内容,我们可以知道,TCPReceiver
的功能之一就是,将数据包TCPSegment
拆分成一个个data
,并且通过seq
生成出这些data
的index
,然后传递给StreamReassembler
。
在说明TCPReceiver
的其他功能前,不妨先从外面的TCPConnection
说起,由外而内回忆一下整个TCP协议过程。
这期间最关键需要理解的,是SYN FIN ACK ack seq这些东西究竟是什么东西。
-seq用来标识字节流中某个字节的序号,在TCP报文中,它表示的是该报文携带的数据的第一个字节的序号。
-与我们在Lab1实现的StreamReassembler
的参数index相比,它有三方面不同:
seq为32位,index为64位
-当一个字节流的数据超过2^32字节(实际上比这少就会环绕)时,seq就会产生环绕。如,当前seq为0xFFFFFFFF,则下一个seq就是0x00000000。
-seq不从0开始,index从0开始
-为了确保传输过程中的安全性,一个字节流的起始seq不为0,而是一个随机数,称其为ISN。
-seq有不携带数据的两个逻辑报文SYN和FIN,index没有
-SYN是TCP“三握手”中服务器端接收到的来自客户端的第一个报文。它是TCP报文中的一个标识位:
-它用以标识数据传输的开始,并且携带seq最初随机的序号ISN。
---除了确保收到所有字节的数据外,TCP必须确保也能收到流的开始和结束
-这个说得非常好,完美解释了为什么需要占据一个seqno
-
ACK也是一个标识位,它代表当前报文是一个确认收到的报文ACK,也即报文中的ackno值有效。
-ack表示当前endpoint【包括客户端和服务器端】希望接收到的下一个数据流的起始字节的seq。
---关于seq和ack,听起来还是有点抽象,不如以连接释放图中ack和seq的值变化为例来说明。
--
为什么一开始seq=u,ack=v,但下一个就是seq=v,ack=u+1?
-这是因为,ack和seq的语义对于客户端和服务器端都是不变的。ack为已经收到的数据的seq+1表示第一个应该接收的值,seq为已经发送的数据的seq+1表示已经发送的值。并且还需要意识到,图中其实有两个数据流(一个是C→S,另一个是S→C),也即有两套seq和ack。
--
-- -
ack
-服务器从客户端接收信号。ack表示服务器希望接收到的下一个序列号,也即为它从客户端收到的数据的seq+1。
-对于此情况,虽然终止报文不携带数据,但其依然占据一个序列号seq。
-因而服务器的ack=u+1.
-- -
seq
-服务器向客户端发送数据。ack表示客户端希望接收到的下一个序号,因而服务器端就应该发送ack这个序号的数据,也即v。
-
FIN也是一个标识位,标识着数据传输的结束
-因而,从图中可以看出,TCP连接中大概会收到以下几类报文:
-特殊报文
-SYN = 1
-C的连接请求 携带了ISN
-S的连接请求确认,ACK = 1,携带了S的ISN
-ACK = 1
-额我觉得这是TCPSender管的。这大概是Connection知道了之后通知下TCPSender吧,应该跟我们这次实验没关系
-FIN = 1
-普通的数据
-我们的TCPReceiver
需要负责TCP协议中部分关键对象的管理。我们需要生成ackno以及拥塞窗口大小;我们需要接收SYN和FIN等信号;我们需要对seq进行处理,将其变为StreamReassembler
所想要的index。
处理数据
-把Internet过来的一个个TCP报文变成一个个小data,小data再由整流器整流为完整的data,外界再通过socket从ByteStream读取完整的data。
-反馈信息
-向发送方反馈自己当前的一些状态信息,如拥塞窗口的大小以及ack等。
-ackno
-本质上是“index of the first unassembled byte”
-window size
-本质上是“the distance between the first unassembled index and the first unacceptable index”
+domain
+在本次实验中只会取值前两个,即本地通信和IPv4网络通信
+也即,ackno为拥塞窗口的左端点,ackno+window_size为拥塞窗口的右端点
+type
+好像比如说取SOCK_DGRAM
就是UDP,取SOCK_STREAM
就是TCP。
从Overview中可以看出来,至关重要的一点就是,将环绕的32bit的seq转化为我们在StreamReassembler
中使用的index。
我们不妨再引入一个中间变量abstract seqno
。则seqno
、abstract seqno
、stream index
三者关系如下图:
显然从seqno
转化为abstract seqno
更加复杂。因而,我们要做的第一个实验部分就是实现这个转化。
我们需要实现类WrappingInt32
。它的wrap
函数将64位的abstract seqno
转化为32位的seqno
,它的unwrap
将32位的seqno
转化为64位的abstract seqno
。
这个实验完美地触及到了我的雷点:对这种环绕来环绕去的东西非常头疼……因而昨天晚上做的时候晕晕乎乎的什么也思考不了,今天过来边画了下图才知道要怎么做。
-wrap
很简单我就不说了。对于unwrap
,我的做法是,先让checkpoint和n-isn都处在同一个区间(红圈)内【也即都让它们对2^32取余】,再通过几个东西之间的关系来确定最终的res是否需要+-HEAD_ONE:
【蓝线表示n-isn,橙线表示红圈区间的中点】
-具体的就不多说了。直接看下面的代码,多画画图就能明白了。
-我一开始头晕晕地去写,对很多地方产生了疑问,激情地写下了一些消极的话语。刚刚出去吹了会儿风回来,bug全都改对了,于是狂喜着把消极的话语全部删掉了()
-怎么说呢,我的错误发生是因为我没有意识到sponge的TCP也许算是一个“简化版”。
-在学习本章内容之前,我特地先去回顾了下TCP协议的全过程,并且所有的SYN,FIN等等等概念都是按照网上的概念来的。因而我在面对自己的错误时真的是一脸懵逼……好在,吹完风之后我还是及时醒悟了。
-思路还是很简单的,细节也不像Lab1那样那么多那么破防,就是一些奇奇怪怪的恶心小毛病太多了,导致我出错频频,并且都是些很sb的问题,让人直接心态爆炸。
-先不吐槽了,接下来就来讲讲总体的思路,以及我产生疑惑的一些地方吧。
-得益于Lab1那个复杂算法的健壮性和多功能性,我们对TCPReceiver
的实现就可以变得更加简洁。我们不再需要关心报文是否能够被成功接收、报文是否重叠等等等。我们仅需对SYN和FIN这样的报文做特殊的参数处理,将seqno转化为index,然后直接传入我们的StreamReassembler
中就行了。
也即,基本流程为:
-// SYN |
/* Socket */ |
SYN很直观,没什么好说的。
-FIN比较烧。之所以不是这么写:
-if(header.fin){ |
上面那俩类其实就是两个包装类,用来将系统调用包装为c++类,看起来很抽象很迷惑。但到这就不一样了!我们开始用上我们之前写的TCP协议的代码了!
+除了跟fd以及socket一致的read
、write
以及close
之外,TCPSocket
最独特的功能,应该就是TCP连接的建立与释放了,其状态转移等逻辑已由我们在Lab0-4实现,此socket类仅实现事件的监听和TCP协议对象生命周期的管理。
在详细说明其两个功能——事件监听和生命周期管理——之前,不妨先了解下其总体的架构。
+TCPSpongeSocket
需要双线程实现。其中一个线程用来招待其owner:它会执行向owner public的connect、read、write等服务。另一个线程用来运行TCPConnection
:它会时刻调用connection的tick方法,并且进行事件监听。
//! \class TCPSpongeSocket |
也即一发现FIN报文到了就++,是因为可能会发生这种情况:
-也即FIN报文虽然到了,但是中间有一段数据还没到,ack应该等于中间那段数据的开头,你这时候想要跳过FIN而把ack+1那肯定是不对的。
-也因而,我们需要记录fin是否有过,并且仅当:
-bool StreamReassembler::empty() const { return buffer.empty()&&is_eof; } |
完成事件监听的核心部分是方法_tcp_loop
以及_initialize_TCP
中对_eventloop
的初始化,还有eventloop
的实现。
看下来其实理解难度不大(虽然细节很多并且我懒得研究了),但我认为很值得学习。
+主要功能是添加我们想监听的事件,有四个,分别是从app得到数据、有要向app发送的数据、从底层协议得到数据、有要向底层协议发送的数据。具体的话,代码和注释都写得很详细就不说了。
+可以看到,TCP与协议栈交互【包括收发数据报】,是通过AdaptT _datagram_adapter;
实现的;TCP与上层APP交互【包括传送数据】,是通过LocalStreamSocket _thread_data;
实现的。
template <typename AdaptT> |
成立时,才能表示数据传输真正结束,让ack++。
-说实话我一开始ackno的数据结构是WrappingInt32。为了这么搞,我还得特地维护一个checkpoint变量用来做unwrap的参数,然后ackno也不能用_reassembler.get_left_bound()
来获取,总之就搞得非常非常麻烦。这时候我不小心【是故意的还是不小心的?】看到了感恩的代码,对其用abstract seqno保存ackno这个想法大为赞叹,于是就果断地沿用了()果然设计思想方面我还是有很大不足啊。
我一开始被这个图以及百度得到的结果受影响:
-认为SYN报文不能携带数据【同理FIN也是】,因而在最初实现的时候看到test case人都麻透了开始怀疑人生……
-不过这也怪我没有意识到实验和业界可能是不一样的,但指导书也没说SYN和FIN到底会不会携带数据……emm,我感觉这一点做得不够详细,也许可以改进一下。
-我现在还是搞不懂这东西究竟是什么玩意……
-指导书上是这么说的:
+可以看到,_tcp_loop
的功能就是,在condition
为真的时候,一是监听我们之前塞进_event_loop
的所有事件,二是调用TCPConnection
的tick
方法来管理时间。
// condition is a function returning true if loop should continue |
eventloop具体是通过Linux提供的poll机制来进行事件监听的。
--the distance between the “first unassembled” index and the “first unacceptable” index.
-This is called the “window size”.
+ +怎么说,又一次感受到了“网络就是IO”这个抽象的牛逼之处。操作系统的poll机制和poll函数本质上是针对IO读写来设计的,而正因为网络的本质是IO,正因为网络收发数据包、与上层app交互本质还是IO(因为通过文件描述符),才能在这里采用这种方式进行文件读写。
+我的评价是佩服到五体投地好吧
++
poll函数就是IO等待的一种实现机制。
++ +
int poll(struct pollfd *fds, nfds_t nfds, int timeout);事件类型events可以为下列值:
+
POLLIN:有数据可读
POLLRDNORM:有普通数据可读,等效于POLLIN
POLLRDBAND:有优先数据可读
POLLPRI:有紧迫数据可读
POLLOUT:写数据不会导致阻塞
POLLWRNORM:写普通数据不会导致阻塞
POLLWRBAND:写优先数据不会导致阻塞
POLLMSG:SIGPOLL消息可用
POLLER:指定的文件描述符发生错误
POLLHUP:指定的文件描述符挂起事件
POLLNVAL:无效的请求,打不开指定的文件描述符
所谓的“first unassembled”正是ackno。而,我正是理解错了所谓“first unacceptable” 的意思,才导致我想了好久好久都没想出来,最后看了答案被薄纱到现在。
-看到这个“first unacceptable” ,我的第一反应就是,维护一个变量right_bound,当packet过来的时候,如果packet的index范围(seqno + data.length())比right_bound大就更新。我认为这才叫做“first unacceptable”。但其实!我会这么想是因为我英语不好……
-“first unacceptable” ,unacceptable,意为无法接受的,也就是说,它跟容量有关。第一个无法接受的,就是第一个超出容量的。而结合我们上面的那张图:
-可以看出,事实上window size就是黑框部分,也即紫框部分减去绿色部分,也即ByteStream
的remaining_capacity()
……
而我以为它是还未收到的的意思,故而才理解成了上面那样。
-看来英语不好也是原罪23333
-//! Transform an "absolute" 64-bit sequence number (zero-indexed) into a WrappingInt32 |
我们在前面的eventloop的rule初始化中:
+_eventloop.add_rule(_datagram_adapter, |
class TCPReceiver { |
这个的意思是针对_datagram_adapter
这个文件的Direction::In
这个事件发生时,就会执行[&]
中的事件。那么Direction::In
是什么?
enum class Direction : short { |
void TCPReceiver::segment_received(const TCPSegment &seg) { |
可见,eventloop具体是通过os提供的IO事件机制来进行监听的。
+具体的监听以及执行逻辑由wait_next_event
来实现。它主要干的就是,清理掉那些我们不感兴趣的或者已经似了(比如说对应的fd已经close之类的)的事件,然后找到那些触发到了的active的事件并且调用它们的caller。
具体代码还是有些微复杂的,有兴趣可以去看看,这里就不放了。
+核心部分为方法connect
、listen_and_accept
以及_tcp_main
。
由客户端调用。
+// Client调用 |
负责establish状态的监听以及之后关闭TCP连接的擦屁股工作
+template <typename AdaptT> |
由服务器端调用。
+// Server调用 |
主菜(上面那个)已经说完了,这两个就是简单的包装类,没什么好说的,大概就做了点传参工作,主要差异还是adapter。
+在我们的TCPSpongeSocket
实现中,我们引入了“adapter”的概念。
protected: |
它很完美地以策略模式的形式,凝结出了我们本次实验所需的各种协议栈的共同代码,放进了TCPSpongeSocket
,而将涉及到协议栈差异的部分用adapter完成。
在TCPSpongeSocket
中,adapter主要完成了如下操作:
adapter的tick函数
+// in tcp_loop |
作为订阅事件的IO流
+_eventloop.add_rule(_datagram_adapter, |
TCP层通过对其读写来获取TCP segment
+auto seg = _datagram_adapter.read(); |
记录各类参数
+datagram_adapter.config().destination.to_string() |
具体实现说实话没什么好说的,确实无非也就是上面那几个方法,然后在里面包装下和操作系统提供的tun和tap的接口交互罢了,代码也比较简单,此处就不说了。
+除了对协议栈的实现之外,在app文件夹下还有许多对我们实现的协议栈的应用实例。我认为了解下应用实例也是很重要的。
+其作用就是建立stdin/stdout与socket的关联。它从stdin读输入,作为上层app的输入写入socket;从socket读输出,传给上层app,也即stdout输出。它的具体实现在stdin/stdout之间隔了两条bytestream,分别是_inbound
和_outbound
。
由于stdin、stdout、socket本质上都是fd,所以我们依然可以采用跟上面一样的事件驱动方式。我们只需在socket有输出时马上读给inbound bytestream,在inbound bytestream有输入时马上读给stdout,在stdin有输入时马上写入outbound bytestream,在outbound bytestream有输入时马上读给socket。遵守这4条rule就行了。
+因而,具体实现就是TCPSpongeSocket::_initialize_TCP
和TCPSpongeSocket::_tcp_loop
的结合体,订阅事件+循环等待。由于跟前面类似,在此就不放代码了。
其他都太复杂了,感觉我水平一般还不大能理解,也懒得看了【草】总之先咕咕咕
]]>http://localhost/webdemo4_war/*.do
。
]]>
在TCP协议中,TCPSender
负责对ack进行处理,将字节流封装为TCP报文,根据拥塞窗口的大小传输数据,以及管理超时重传。
我们的TCPSender
需要做的是:
++本次实验一直在强调的一点就是,TCP的功能是将底层的零散数据包,拼接成一个reliable in-order的byte stream。这个对我来说非常“振聋发聩”(夸张了233),以前只是背诵地知道TCP的可靠性,这次我算是第一次知道了所谓“可靠”究竟可靠在哪:一是保证了序列有序性,二是保证了数据不丢失(从软件层面)。
+还有一个就是大致了解了cs144的主题:实现TCP协议。也就是说,运输层下面的那些层是不用管的吗?不过这样也挺恰好,我正好在学校的实验做过对下面这些层的实现了,就差一个TCP23333这样一来,我的协议栈就可以完整了。
+
++本次实验与TCP的关系:
+在我们的
+webget
实现中,正是由于TCP的可靠传输,才能使我们的http request正确地被服务器接收,才能使服务器的response正确地被我们接收打印。而在
+ByteStream
中,我们也做了跟TCP类似的工作:接收substring,并且将它们拼接为in-order的byte stream【由于在内存中/单线程,所以这个工作看起来非常简单】:+
while(is_input_end == false&&pointer<length){
if(buffer.size() == capacity) break;
buffer.push_back(data[pointer]);
pointer++;
}
主要是介绍了telnet
指令
用的是telnet带smtp参
+上面的telnet是一个client program。接下来我们要把自己放在server的位置上。
+用的是netcat
指令。
这个确实不难,就是这个地方有点坑:
+++Please note that in HTTP, each line must be ended with “\r\n” (it’s not sufficient to use just “\n” or endl).
+
导致我跟400 Bad Request大眼瞪小眼了好久。。。
+void get_URL(const string &host, const string &path) { |
还有一点值得注意的是,当我这样时:
+TCPSocket sock; |
会报错Operation now in progress
。
++关于socket通信中在connect()遇到的Operation now in progress错误
+遇到此错误是因为将在connect()函数之前将套接字socket设为了非阻塞模式。改为在connect()函数之后设置即可。
+
我觉得这个实验设计得挺好的,写的时候感觉很有意思。我推荐看下 https://github.com/shootfirst/CS144/blob/main/lab-0/apps/webget.cc 里的注释,写得很好很规范,让我明白了很多本来没搞懂的地方,比如说shutdown
的用法。
++实现一个
+ByteStream
类,可以通过read
和write
对其两端进行读写。是单线程程序,因而无需考虑阻塞。
这东西其实是很简单的,但是我还是花了一定的时间,主要原因有两点,一是我不懂c++,所以一些地方错得我很懵逼,二是因为我是sb。
+下面就记录下三个我印象比较深刻的错误吧。
+构造函数我一开始是这么写的:
+结果爆出了这样的错:
+搜了半天也没看懂怎么回事,去求助了下某场外c艹选手,才知道了还有成员变量初始化列表这玩意,这个东西似乎比较高效安全。
+于是我改成了这么写:
+它告诉我buffer
也得初始化。于是我又这么写:
又是奇奇怪怪的错误,说明vector不能这么初始化。
+场外c艹选手看到了这个:
+所以说vector应该这样初始化:
+vector
作为buffer的载体应该使用的是可以从front删除数据的数据结构,比如说deque。【vector也行,但是效率较低】
+具体为什么,可以以数据流为cat为例。执行peek(2)
时,使用vector得到的是at,使用deque得到的是ca。
一开始在write
方法,我是这么写的:
int length = data.length(); |
结果就是测试用例Timeout。我找了很久都不知道错在了哪,最后求助了场外观众【罪过……这次实验太不独立了】,学着他把length改成了这样:
+int length = min(data.length(),capacity-buffer.size()); |
发现成了。
+我去看了看testbench,猜测应该是因为阻塞了,我还以为是deque自身会阻塞【是的,我完全没注意到自己顺手把阻塞写了下去】,查了半天发现不会,最后才发现是自己不小心搞错了呃呃…………
+class ByteStream { |
ByteStream::ByteStream(const size_t cap) : total_write(0),total_read(0),is_input_end(false),capacity(cap),buffer(){ } |
在前两个实验中,我们可以说只是做了点算法上的抽象工作,跟TCP协议还是没什么显著的关系的。但来到了本次实验,一切就都不一样了。
+依然还是这张图。相信此时做过前两个实验之后,看到这张图就会有了不一样的发现。
+我们对TCP协议的实现是由内向外的,先实现里面再实现最外层。前两节实验,我们由内而外实现了ByteStream
和StreamReassembler
;在这次实验中,我们会实现更外层一点的TCPReceiver
。
根据我们前两次实验内容,我们可以知道,TCPReceiver
的功能之一就是,将数据包TCPSegment
拆分成一个个data
,并且通过seq
生成出这些data
的index
,然后传递给StreamReassembler
。
在说明TCPReceiver
的其他功能前,不妨先从外面的TCPConnection
说起,由外而内回忆一下整个TCP协议过程。
这期间最关键需要理解的,是SYN FIN ACK ack seq这些东西究竟是什么东西。
+seq用来标识字节流中某个字节的序号,在TCP报文中,它表示的是该报文携带的数据的第一个字节的序号。
+与我们在Lab1实现的StreamReassembler
的参数index相比,它有三方面不同:
维护拥塞窗口
-我们需要通过ackno和window_size两个参数维护拥塞窗口的大小
-填充拥塞窗口
-必须as possible。除非拥塞窗口满或者ByteStream
空才不填。
对于从ByteStream
读出的数据,我们需要把其封装为一个TCPSegment
再向_segment_out
输出
seq为32位,index为64位
+当一个字节流的数据超过2^32字节(实际上比这少就会环绕)时,seq就会产生环绕。如,当前seq为0xFFFFFFFF,则下一个seq就是0x00000000。
记录哪一部分ack了,哪一部分没有ack
-我们需要在发送segment的同时暂存segment,当且仅当接收到ack,并且ack为segment.seqno+length的时候才能将其释放。
+seq不从0开始,index从0开始
+为了确保传输过程中的安全性,一个字节流的起始seq不为0,而是一个随机数,称其为ISN。
管理超时重传
-当对方超过一段时间还没有收到数据时,需要进行超时重传
-以segment为单位,一个segment重传具有原子性。
-在sender和暂存segment的数据结构中保存时钟滴答
+seq有不携带数据的两个逻辑报文SYN和FIN,index没有
特别的,指导书上有一段话表述得很有意思:
-这体现了TCPReceiver
和TCPSender
之间的对偶关系,这种细节性的设计理念值得学习。
写完TCPSender
后我还是觉得有些迷茫……就跟TCPReceiver
一样。说不出来具体是哪里不清楚,但总感觉隐隐约约有些怪怪的?总感觉相互之间接口有点混乱,对它们之间是怎么交互的一概不知。我想这是由于我们是自底向上实现TCP协议所带来的问题。希望这种感觉在实现完TCPConnection
之后可以好转吧。
TCPReceiver
的主要任务是把segment拼接成字节流,以及维护即将要告知TCPSender
的ackno和拥塞窗口大小。而TCPSender
的作用就是把字节流切成segment,并且根据ackno和拥塞窗口大小,进行数据的填充以及超时重传的管理。可以看到,它们是对偶的关系。
看完指导书以及各种接口定义可以得知,我们需要:
-增加成员变量
-window_size 拥塞窗口的大小
-ackono 记录当前收到的最大ackno
-ticks 记录sender从出生到现在的时钟滴答
+SYN是TCP“三握手”中服务器端接收到的来自客户端的第一个报文。它是TCP报文中的一个标识位:
+它用以标识数据传输的开始,并且携带seq最初随机的序号ISN。
+++除了确保收到所有字节的数据外,TCP必须确保也能收到流的开始和结束
+这个说得非常好,完美解释了为什么需要占据一个seqno
+
ACK也是一个标识位,它代表当前报文是一个确认收到的报文ACK,也即报文中的ackno值有效。
+ack表示当前endpoint【包括客户端和服务器端】希望接收到的下一个数据流的起始字节的seq。
+++关于seq和ack,听起来还是有点抽象,不如以连接释放图中ack和seq的值变化为例来说明。
++
为什么一开始seq=u,ack=v,但下一个就是seq=v,ack=u+1?
+这是因为,ack和seq的语义对于客户端和服务器端都是不变的。ack为已经收到的数据的seq+1表示第一个应该接收的值,seq为已经发送的数据的seq+1表示已经发送的值。并且还需要意识到,图中其实有两个数据流(一个是C→S,另一个是S→C),也即有两套seq和ack。
++
- -
ack
+服务器从客户端接收信号。ack表示服务器希望接收到的下一个序列号,也即为它从客户端收到的数据的seq+1。
+对于此情况,虽然终止报文不携带数据,但其依然占据一个序列号seq。
+因而服务器的ack=u+1.
tmp_size 记录tmp_segments 中的数据字节数(注意算上SYN和FIN)
+- -
seq
+服务器向客户端发送数据。ack表示客户端希望接收到的下一个序号,因而服务器端就应该发送ack这个序号的数据,也即v。
tmp_segments 暂存segment,等待收到ack
-数据结构:
-list,自定义struct,结构体内有
--
+- TCPSegment
-- seqno 记录该segment的起始数据的seq
-- data_size 记录该segment携带数据的长度
FIN也是一个标识位,标识着数据传输的结束
+因而,从图中可以看出,TCP连接中大概会收到以下几类报文:
+特殊报文
+SYN = 1
+C的连接请求 携带了ISN
cons_retran 记录连续的超时重传次数
+S的连接请求确认,ACK = 1,携带了S的ISN
syn 标记当前是否为第一个segment
+fin
+ACK = 1
+额我觉得这是TCPSender管的。这大概是Connection知道了之后通知下TCPSender吧,应该跟我们这次实验没关系
rto 记录当前的RTO
+FIN = 1
timer_start 记录timer是否等待中
+timer_ticks 记录timer开启时的时间
+普通的数据
我们的TCPReceiver
需要负责TCP协议中部分关键对象的管理。我们需要生成ackno以及拥塞窗口大小;我们需要接收SYN和FIN等信号;我们需要对seq进行处理,将其变为StreamReassembler
所想要的index。
处理数据
+把Internet过来的一个个TCP报文变成一个个小data,小data再由整流器整流为完整的data,外界再通过socket从ByteStream读取完整的data。
实现一个定时函数
-第一次从bytestream取出数据包装为segment的时候(也即发送SYN报文)开启它,当所有data都收到ack的时候(也即FIN报文也被成功ACK)关闭它
-应该在ticks中被调用
---Every time a segment containing data (nonzero length in sequence space) is sent (whether it’s the first time or a retransmission), if the timer is not running, start it running so that it will expire after RTO milliseconds.
-
当timer触发时,我们需要重传tmp_segments 队列头。
-如果空间足够,直接重传就行了,然后double RTO,然后用RTO reset timer,然后再次启动timer。
-如果空间不足够,只做上面那个的后两步,也即reset timer,然后再次启动timer。
-ack_received
反馈信息
+向发送方反馈自己当前的一些状态信息,如拥塞窗口的大小以及ack等。
更新window_size和ackno
-重置超时重传
-如果接收到的ackno比以前的大,则重置RTO,重启timer(如果tmp_segments不为空),重置cons_retran
-从tmp_segments中删除元素
+ackno
+本质上是“index of the first unassembled byte”
调用fill_window
window size
+本质上是“the distance between the first unassembled index and the first unacceptable index”
fill_window
如果window_size - tmp_size <= 0 或者 byte stream空,则什么也不做
-否则根据syn和fin标记创建一个new segment,然后写入out stream
---no bigger than the value given by TCPConfig::MAX PAYLOAD SIZE (1452 bytes)
-
-+If the receiver has announced a window size of zero, the fifill window method should act like the window size is one.
-
也即,ackno为拥塞窗口的左端点,ackno+window_size为拥塞窗口的右端点
实现起来虽然很复杂,但思路确实很简单,正确思路和初见思路差不多,指导书写得很好很详细【以至于一开始我被指导书这么多内容给吓到了】。在这里只记录点实现过程中遇到的一些小错误以及我各个部分的实现细节补充。
-指导书的建议是实现一个类,但是我太懒了()而且确实这个timer的状态也很少,因而我就直接把它写在sender里面了。
-此实验未涉及这个。本次全部的测试用例都是SYN报文不携带数据的情况。【因为发出syn报文之后才将window_size设置为非0情况】
-如果需要SYN报文不携带数据,可以在fill_window
中把这句话:
if (!(_stream.buffer_empty() || remaining == 0)) { |
从Overview中可以看出来,至关重要的一点就是,将环绕的32bit的seq转化为我们在StreamReassembler
中使用的index。
我们不妨再引入一个中间变量abstract seqno
。则seqno
、abstract seqno
、stream index
三者关系如下图:
显然从seqno
转化为abstract seqno
更加复杂。因而,我们要做的第一个实验部分就是实现这个转化。
我们需要实现类WrappingInt32
。它的wrap
函数将64位的abstract seqno
转化为32位的seqno
,它的unwrap
将32位的seqno
转化为64位的abstract seqno
。
这个实验完美地触及到了我的雷点:对这种环绕来环绕去的东西非常头疼……因而昨天晚上做的时候晕晕乎乎的什么也思考不了,今天过来边画了下图才知道要怎么做。
+wrap
很简单我就不说了。对于unwrap
,我的做法是,先让checkpoint和n-isn都处在同一个区间(红圈)内【也即都让它们对2^32取余】,再通过几个东西之间的关系来确定最终的res是否需要+-HEAD_ONE:
【蓝线表示n-isn,橙线表示红圈区间的中点】
+具体的就不多说了。直接看下面的代码,多画画图就能明白了。
+我一开始头晕晕地去写,对很多地方产生了疑问,激情地写下了一些消极的话语。刚刚出去吹了会儿风回来,bug全都改对了,于是狂喜着把消极的话语全部删掉了()
+怎么说呢,我的错误发生是因为我没有意识到sponge的TCP也许算是一个“简化版”。
+在学习本章内容之前,我特地先去回顾了下TCP协议的全过程,并且所有的SYN,FIN等等等概念都是按照网上的概念来的。因而我在面对自己的错误时真的是一脸懵逼……好在,吹完风之后我还是及时醒悟了。
+思路还是很简单的,细节也不像Lab1那样那么多那么破防,就是一些奇奇怪怪的恶心小毛病太多了,导致我出错频频,并且都是些很sb的问题,让人直接心态爆炸。
+先不吐槽了,接下来就来讲讲总体的思路,以及我产生疑惑的一些地方吧。
+得益于Lab1那个复杂算法的健壮性和多功能性,我们对TCPReceiver
的实现就可以变得更加简洁。我们不再需要关心报文是否能够被成功接收、报文是否重叠等等等。我们仅需对SYN和FIN这样的报文做特殊的参数处理,将seqno转化为index,然后直接传入我们的StreamReassembler
中就行了。
也即,基本流程为:
+// SYN |
修改为这句话:
-if (!segment.header().syn&&!(_stream.buffer_empty() || remaining == 0)) { |
SYN很直观,没什么好说的。
+FIN比较烧。之所以不是这么写:
+if(header.fin){ |
class TCPSender { |
也即一发现FIN报文到了就++,是因为可能会发生这种情况:
+也即FIN报文虽然到了,但是中间有一段数据还没到,ack应该等于中间那段数据的开头,你这时候想要跳过FIN而把ack+1那肯定是不对的。
+也因而,我们需要记录fin是否有过,并且仅当:
+bool StreamReassembler::empty() const { return buffer.empty()&&is_eof; } |
TCPSender::TCPSender(const size_t capacity, const uint16_t retx_timeout, const std::optional<WrappingInt32> fixed_isn) |
成立时,才能表示数据传输真正结束,让ack++。
+说实话我一开始ackno的数据结构是WrappingInt32。为了这么搞,我还得特地维护一个checkpoint变量用来做unwrap的参数,然后ackno也不能用_reassembler.get_left_bound()
来获取,总之就搞得非常非常麻烦。这时候我不小心【是故意的还是不小心的?】看到了感恩的代码,对其用abstract seqno保存ackno这个想法大为赞叹,于是就果断地沿用了()果然设计思想方面我还是有很大不足啊。
我一开始被这个图以及百度得到的结果受影响:
+认为SYN报文不能携带数据【同理FIN也是】,因而在最初实现的时候看到test case人都麻透了开始怀疑人生……
+不过这也怪我没有意识到实验和业界可能是不一样的,但指导书也没说SYN和FIN到底会不会携带数据……emm,我感觉这一点做得不够详细,也许可以改进一下。
+我现在还是搞不懂这东西究竟是什么玩意……
+指导书上是这么说的:
+++the distance between the “first unassembled” index and the “first unacceptable” index.
+This is called the “window size”.
+
所谓的“first unassembled”正是ackno。而,我正是理解错了所谓“first unacceptable” 的意思,才导致我想了好久好久都没想出来,最后看了答案被薄纱到现在。
+看到这个“first unacceptable” ,我的第一反应就是,维护一个变量right_bound,当packet过来的时候,如果packet的index范围(seqno + data.length())比right_bound大就更新。我认为这才叫做“first unacceptable”。但其实!我会这么想是因为我英语不好……
+“first unacceptable” ,unacceptable,意为无法接受的,也就是说,它跟容量有关。第一个无法接受的,就是第一个超出容量的。而结合我们上面的那张图:
+可以看出,事实上window size就是黑框部分,也即紫框部分减去绿色部分,也即ByteStream
的remaining_capacity()
……
而我以为它是还未收到的的意思,故而才理解成了上面那样。
+看来英语不好也是原罪23333
+//! Transform an "absolute" 64-bit sequence number (zero-indexed) into a WrappingInt32 |
class TCPReceiver { |
void TCPReceiver::segment_received(const TCPSegment &seg) { |
http://localhost/webdemo4_war/*.do
。
]]>
本次实验要实现的是IP层的路由工作,但是只用实现对路由表进行操作的部分,比如说增加表项以及查询路由表等,其他的什么RIP、OSPF都不用我们实现,所以这样一来其实就简单非常多了()
-有一点需要注意的是,它一直在强调一个“最长前缀匹配”。也就是:
-还有一点需要注意的是路由的结构:
-实际上就是路由表+一堆网络接口,这些端口都是network interface。
+在TCP协议中,TCPSender
负责对ack进行处理,将字节流封装为TCP报文,根据拥塞窗口的大小传输数据,以及管理超时重传。
我们的TCPSender
需要做的是:
维护拥塞窗口
+我们需要通过ackno和window_size两个参数维护拥塞窗口的大小
+填充拥塞窗口
+必须as possible。除非拥塞窗口满或者ByteStream
空才不填。
对于从ByteStream
读出的数据,我们需要把其封装为一个TCPSegment
再向_segment_out
输出
记录哪一部分ack了,哪一部分没有ack
+我们需要在发送segment的同时暂存segment,当且仅当接收到ack,并且ack为segment.seqno+length的时候才能将其释放。
+管理超时重传
+当对方超过一段时间还没有收到数据时,需要进行超时重传
+以segment为单位,一个segment重传具有原子性。
+在sender和暂存segment的数据结构中保存时钟滴答
+特别的,指导书上有一段话表述得很有意思:
+这体现了TCPReceiver
和TCPSender
之间的对偶关系,这种细节性的设计理念值得学习。
写完TCPSender
后我还是觉得有些迷茫……就跟TCPReceiver
一样。说不出来具体是哪里不清楚,但总感觉隐隐约约有些怪怪的?总感觉相互之间接口有点混乱,对它们之间是怎么交互的一概不知。我想这是由于我们是自底向上实现TCP协议所带来的问题。希望这种感觉在实现完TCPConnection
之后可以好转吧。
TCPReceiver
的主要任务是把segment拼接成字节流,以及维护即将要告知TCPSender
的ackno和拥塞窗口大小。而TCPSender
的作用就是把字节流切成segment,并且根据ackno和拥塞窗口大小,进行数据的填充以及超时重传的管理。可以看到,它们是对偶的关系。
看完指导书以及各种接口定义可以得知,我们需要:
+增加成员变量
+window_size 拥塞窗口的大小
+ackono 记录当前收到的最大ackno
+ticks 记录sender从出生到现在的时钟滴答
+tmp_size 记录tmp_segments 中的数据字节数(注意算上SYN和FIN)
+tmp_segments 暂存segment,等待收到ack
+数据结构:
+list,自定义struct,结构体内有
+cons_retran 记录连续的超时重传次数
+syn 标记当前是否为第一个segment
+fin
+rto 记录当前的RTO
+timer_start 记录timer是否等待中
+timer_ticks 记录timer开启时的时间
+实现一个定时函数
+第一次从bytestream取出数据包装为segment的时候(也即发送SYN报文)开启它,当所有data都收到ack的时候(也即FIN报文也被成功ACK)关闭它
+应该在ticks中被调用
--路由器可分为两部分,一部分控制路由协议,包括完善路由表之类的;另一部分负责数据转发。
-负责接收数据的端口既可能收到数据,也可能收到路由信息报文。收到前者,则需要查询转发表然后进行路由转发;收到后者,就需要将其交付给路由选择处理机进行处理。
-它有一个地方说得很有意思:路由表需要对网络拓扑最优化,转发表需要使查找过程最优化
-也就是说,路由表只是key为目的IP地址,value为下一跳IP地址的一个普通map,可以是unordered_map,因为无需对它进行查找操作;转发表的内容可能跟路由表差不多,但是由于它要被进行频繁的查找工作,因而其数据结构需要对查找的消耗较低。
-不过在我们这边,一般不区分路由表和转发表的概念。
+Every time a segment containing data (nonzero length in sequence space) is sent (whether it’s the first time or a retransmission), if the timer is not running, start it running so that it will expire after RTO milliseconds.
说实话思路很直观很简单,懒得说了,直接看代码吧【开摆】
-我唯一卡得比较久的有两个地方,一个是一开始数据结构选用的是set,图它的天然排序,针对prefix_length
排序来优化查找,但是没有意识到,对于自定义比较运算符的结构体,set也是会自动去重的()而不同路由项的prefix_length
显然可以重复。因而这样是达咩的,最后不得已选用了一个普通的list。
另一个是子网掩码计算问题,刚开始一个小地方想错了。这个没什么好说的,纯纯脑子一抽。
-// ... |
当timer触发时,我们需要重传tmp_segments 队列头。
+如果空间足够,直接重传就行了,然后double RTO,然后用RTO reset timer,然后再次启动timer。
+如果空间不足够,只做上面那个的后两步,也即reset timer,然后再次启动timer。
+ack_received
更新window_size和ackno
+重置超时重传
+如果接收到的ackno比以前的大,则重置RTO,重启timer(如果tmp_segments不为空),重置cons_retran
+从tmp_segments中删除元素
+调用fill_window
fill_window
如果window_size - tmp_size <= 0 或者 byte stream空,则什么也不做
+否则根据syn和fin标记创建一个new segment,然后写入out stream
+++no bigger than the value given by TCPConfig::MAX PAYLOAD SIZE (1452 bytes)
+
++If the receiver has announced a window size of zero, the fifill window method should act like the window size is one.
+
实现起来虽然很复杂,但思路确实很简单,正确思路和初见思路差不多,指导书写得很好很详细【以至于一开始我被指导书这么多内容给吓到了】。在这里只记录点实现过程中遇到的一些小错误以及我各个部分的实现细节补充。
+指导书的建议是实现一个类,但是我太懒了()而且确实这个timer的状态也很少,因而我就直接把它写在sender里面了。
+此实验未涉及这个。本次全部的测试用例都是SYN报文不携带数据的情况。【因为发出syn报文之后才将window_size设置为非0情况】
+如果需要SYN报文不携带数据,可以在fill_window
中把这句话:
if (!(_stream.buffer_empty() || remaining == 0)) { |
void Router::add_route(const uint32_t route_prefix, |
修改为这句话:
+if (!segment.header().syn&&!(_stream.buffer_empty() || remaining == 0)) { |
class TCPSender { |
TCPSender::TCPSender(const size_t capacity, const uint16_t retx_timeout, const std::optional<WrappingInt32> fixed_isn) |
http://localhost/webdemo4_war/*.do
。
]]>
记录一些git的原理学习,以及工作学习中遇到的一些git的操作问题。
+本次实验要实现的是IP层的路由工作,但是只用实现对路由表进行操作的部分,比如说增加表项以及查询路由表等,其他的什么RIP、OSPF都不用我们实现,所以这样一来其实就简单非常多了()
+有一点需要注意的是,它一直在强调一个“最长前缀匹配”。也就是:
+还有一点需要注意的是路由的结构:
+实际上就是路由表+一堆网络接口,这些端口都是network interface。
++-路由器可分为两部分,一部分控制路由协议,包括完善路由表之类的;另一部分负责数据转发。
+负责接收数据的端口既可能收到数据,也可能收到路由信息报文。收到前者,则需要查询转发表然后进行路由转发;收到后者,就需要将其交付给路由选择处理机进行处理。
+它有一个地方说得很有意思:路由表需要对网络拓扑最优化,转发表需要使查找过程最优化
+也就是说,路由表只是key为目的IP地址,value为下一跳IP地址的一个普通map,可以是unordered_map,因为无需对它进行查找操作;转发表的内容可能跟路由表差不多,但是由于它要被进行频繁的查找工作,因而其数据结构需要对查找的消耗较低。
+不过在我们这边,一般不区分路由表和转发表的概念。
第一次提pr
-修改提过的pr
-本地仓库与远程同步
-直接修改,然后push到fork的对应分支就行,会自动更新。
-本地仓库与远程不同步
-以下操作都在new-branch分支上
-git rebase --continue
继续rebase修改 git 的历史 commit,你能想到几种方案? 详细介绍了rebase基本用法
-如图,我也不知道为什么突然就寄了。。。
-总之进行了这些操作,虽然不知道是哪个起作用了,但总算好了:
-https://blog.csdn.net/xiaoqixiaoguai/article/details/128591332
-首先删除空白对象
-cd .git |
然后更新ref到某个版本号
-cd .. |
如果还不能用,继续:
-rm .git/index |
我到这里之后显示:
-继续执行:
-修复 refs/remotes/origin/master:
-bashCopy codegit update-ref -d refs/remotes/origin/master |
这将删除损坏的 origin/master
引用,然后从远程仓库重新获取。
修复 dangling blob:
-如果 git fsck
显示了 dangling blob
,你可以尝试删除这些对象:
bashCopy codegit reflog expire --expire=now --all |
说实话思路很直观很简单,懒得说了,直接看代码吧【开摆】
+我唯一卡得比较久的有两个地方,一个是一开始数据结构选用的是set,图它的天然排序,针对prefix_length
排序来优化查找,但是没有意识到,对于自定义比较运算符的结构体,set也是会自动去重的()而不同路由项的prefix_length
显然可以重复。因而这样是达咩的,最后不得已选用了一个普通的list。
另一个是子网掩码计算问题,刚开始一个小地方想错了。这个没什么好说的,纯纯脑子一抽。
+// ... |
这将清理无用的 dangling 对象。
-成功。
-merge与rebase的差异
-merge:
-rebase:
-void Router::add_route(const uint32_t route_prefix, |
http://localhost/webdemo4_war/*.do
。
]]>
其实是9.10就开始了,但9.19才办理入职所以少算了几天xxxx不然这不显得我菜hh
- -也许有许多programer与我一样,在初次接触到“开源”这个概念时,便对其产生了无限的向往。千万人通过共同的事业联结在一起,为了行业的进步不求回报地压缩自己的时间,贡献出自己的一份力,这是何等的浪漫。再加上听了无数遍的Linux发展历程故事,对“开源”我是愈发地憧憬。
-然而,由于学习cs的时间尚不足,也不知道该从何入手去参与社区贡献,尽管心中怀有对参与这份事业的渴望,我还是会“望而却步”。转折点在今年参加竞赛时,在使用某个开源项目时因为遇到了一点问题而去提了个issue。虽然这个问题本质很傻,但在与开发者你来我往的交流中,我深切地感受到自己仿佛离憧憬更近了一步。因而,在今年竞赛结束后的九月,我毫不犹豫地向PLCT Lab提交了简历,试图追逐那个长存我心的幻影。
-而现在,距离第一次提issue已有半年,距离考核通过也已有一个半月之久,我想也是时候好好总结一下我这一个月以来的心路历程了。
---从9.10过审核开始写了很久的日报,当然除了实习还包括别的内容:
-+
记录一些git的原理学习,以及工作学习中遇到的一些git的操作问题。
这一个月以来,我学到了很多东西,学习周期大致可分为三个阶段:初步了解rtt和配置环境、设备树学习以及最后的gpio driver开发。
-由于时间有限,所以仅对rtt的标准版本做了一个比较基本的了解(也就是说没有太涉及到源码部分,只能说是对文档中心的那些对外开发用接口有一定的了解)。rtt是一个微内核的RTOS,这与以前所接触的Linux和xv6都不同。由于RTOS的特性,它的许多设计都十分精简,相比于Linux可谓”麻雀虽小五脏俱全“。
-对rtt的基本介绍,详情可见其文档中心。我印象最深(也是开发过程中接触得最多的)的几个点有:
+第一次提pr
接口设计
-与Linux一样,rtt也采用了精简的接口设计。
+自动初始化机制
-帅的一匹,具体详见此。
+修改提过的pr
+本地仓库与远程同步
+直接修改,然后push到fork的对应分支就行,会自动更新。
本地仓库与远程不同步
+以下操作都在new-branch分支上
+git rebase --continue
继续rebase修改 git 的历史 commit,你能想到几种方案? 详细介绍了rebase基本用法
+如图,我也不知道为什么突然就寄了。。。
+总之进行了这些操作,虽然不知道是哪个起作用了,但总算好了:
+https://blog.csdn.net/xiaoqixiaoguai/article/details/128591332
+首先删除空白对象
+cd .git |
然后更新ref到某个版本号
+cd .. |
如果还不能用,继续:
+rm .git/index |
我到这里之后显示:
+继续执行:
+修复 refs/remotes/origin/master:
+bashCopy codegit update-ref -d refs/remotes/origin/master |
这将删除损坏的 origin/master
引用,然后从远程仓库重新获取。
修复 dangling blob:
+如果 git fsck
显示了 dangling blob
,你可以尝试删除这些对象:
bashCopy codegit reflog expire --expire=now --all |
这将清理无用的 dangling 对象。
+成功。
+merge与rebase的差异
+merge:
+rebase:
+其余的只能说不甚了解,还有待挖掘。
-由于确实对这种东西毫无所知,所以配环境这个过程也是比较漫长,而且很折磨很痛苦(。
-硬件方面,一开始拿到IO-board连这是啥都不知道,还以为这就是开发板研究了半天怎么上电和把内核烧进去(((。然后东西也是买得缺斤少两,比如拿到开发板不知道还要有TF卡,上网搜图研究了半天才意识到;再比如也没有USB-TTL,又是一通淘宝购物。这些各种各样的小白问题导致配环境的周期十分漫长。然后还有一些很傻的错误,再次也不好意思多说了,详情可见rtt硬件环境搭建。
-软件方面倒是没什么问题,之前也早就跟编译内核用的menuconfig打过很多交道,磁盘分区之类的东西之前也简单使过几次,只不过经过这次后也算是使得更加熟练了。
---TODO,这部分还是比较多好写的,虽然还是有点模糊(待我之后有时间整理下放个链接
-
设备树还是比较复杂,而且因为本人的不审慎,导致对其理解出了偏差,还麻烦了社区看我的代码(悲)只能说个人出道的开源社区还是需要对自己的所有笔代码负责。
---pr:
--
涉及到的各种硬件手册:
--
这也是我最后这一周在做的工作,虽说只有短短一周,但是每天都研究这个花了我不少时间和精力()目前算是写完了它的所有功能(大概),并且已经能把LED闪烁和中断绑定函数润起来了,提的pr在这里。
-以前对驱动的理解,还停留在手把手教你做事的xv6的netlab。也因而,这次可以算是以完全一无所知的状态接下了这个任务。
-不过,好在有以前那个短暂的lab经验,我还是稳扎稳打地定下了具体的学习步骤:调研(包括获取各种data book、schematic、rtt官方文档、Linux和rtt相关代码),然后就是学习。
-好在有设备树的研究积淀,我也算是比较快地掌握了milkv上gpio的分布、型号及其地址空间,从而顺藤摸瓜找到了对应gpio型号的Linux驱动代码参考和硬件手册,算是免去了不少麻烦。
-然后,我观察rtt的gpio驱动们,也找到了对应的pin.md文档,了解了下大致的代码框架思路:
-于是接下来的工作也可以比较独立地划分为两部分,一个是数据读写的实现(通过LED闪烁程序测试),另一个是rtt特有的中断回调函数的支持(通过中断程序测试),可以专注对这两个方面开发了。
-其中,数据读写的实现需要对寄存器和引脚号等有所了解。寄存器相对比较简单,只需阅读dwapb的data book即可;而引脚号到gpio的转换则花了我不少时间,做了许多猜想并且进行验证,最后误打误撞地“猜”中了正确思路,通过了LED测试。
-不过引脚号我现在还是不大懂,总之先参照别的bsp写法自己编了个,等着代码review看下吧。
-前期调研一直到LED亮起来花了我整整五六天()相比于此的困难,中断倒显得简单了许多,毕竟它属于是偏软件相关的。调了一天,也从Linux和其他bsp那边抄了些代码,最终在凌晨两点半成功完成了功能测试()今天又花了一个下午整理了下代码和写日记,最终总算是把这个作业交上去了。
-整个过程光是写看起来还是比较轻松,但是由于初次开发摸索,每个小跨越都得花费我不少时间去调研搜索,经常是在长达几个小时的不知所措后才短暂地获得了一些光明,我甚至多次想过要不要去辞职了(((。总之,最后还是坚持了下来。看到蓝色的LED在夜晚的T5中闪烁,我还是十分激动的,眼泪都爆出来了()
-哎,坚持难能可贵,很高兴我最终做到了,虽然尚有测试上的不完善,以及还在等待review。
-总之,这一个月来我学到了许多,同时也对我以前未曾涉足的空白领域做了许多探索,包括对设备树、对嵌入式开发、对Linux设备驱动等的学习,总体来说还是十分甚至九分地开心。希望下个月能再接再厉。
]]>http://localhost/webdemo4_war/*.do
。
]]>
其实是9.10就开始了,但9.19才办理入职所以少算了几天xxxx不然这不显得我菜hh
+ +也许有许多programer与我一样,在初次接触到“开源”这个概念时,便对其产生了无限的向往。千万人通过共同的事业联结在一起,为了行业的进步不求回报地压缩自己的时间,贡献出自己的一份力,这是何等的浪漫。再加上听了无数遍的Linux发展历程故事,对“开源”我是愈发地憧憬。
+然而,由于学习cs的时间尚不足,也不知道该从何入手去参与社区贡献,尽管心中怀有对参与这份事业的渴望,我还是会“望而却步”。转折点在今年参加竞赛时,在使用某个开源项目时因为遇到了一点问题而去提了个issue。虽然这个问题本质很傻,但在与开发者你来我往的交流中,我深切地感受到自己仿佛离憧憬更近了一步。因而,在今年竞赛结束后的九月,我毫不犹豫地向PLCT Lab提交了简历,试图追逐那个长存我心的幻影。
+而现在,距离第一次提issue已有半年,距离考核通过也已有一个半月之久,我想也是时候好好总结一下我这一个月以来的心路历程了。
+++从9.10过审核开始写了很久的日报,当然除了实习还包括别的内容:
++
这一个月以来,我学到了很多东西,学习周期大致可分为三个阶段:初步了解rtt和配置环境、设备树学习以及最后的gpio driver开发。
+由于时间有限,所以仅对rtt的标准版本做了一个比较基本的了解(也就是说没有太涉及到源码部分,只能说是对文档中心的那些对外开发用接口有一定的了解)。rtt是一个微内核的RTOS,这与以前所接触的Linux和xv6都不同。由于RTOS的特性,它的许多设计都十分精简,相比于Linux可谓”麻雀虽小五脏俱全“。
+对rtt的基本介绍,详情可见其文档中心。我印象最深(也是开发过程中接触得最多的)的几个点有:
+接口设计
+与Linux一样,rtt也采用了精简的接口设计。
+自动初始化机制
+帅的一匹,具体详见此。
+其余的只能说不甚了解,还有待挖掘。
+由于确实对这种东西毫无所知,所以配环境这个过程也是比较漫长,而且很折磨很痛苦(。
+硬件方面,一开始拿到IO-board连这是啥都不知道,还以为这就是开发板研究了半天怎么上电和把内核烧进去(((。然后东西也是买得缺斤少两,比如拿到开发板不知道还要有TF卡,上网搜图研究了半天才意识到;再比如也没有USB-TTL,又是一通淘宝购物。这些各种各样的小白问题导致配环境的周期十分漫长。然后还有一些很傻的错误,再次也不好意思多说了,详情可见rtt硬件环境搭建。
+软件方面倒是没什么问题,之前也早就跟编译内核用的menuconfig打过很多交道,磁盘分区之类的东西之前也简单使过几次,只不过经过这次后也算是使得更加熟练了。
+++TODO,这部分还是比较多好写的,虽然还是有点模糊(待我之后有时间整理下放个链接
+
设备树还是比较复杂,而且因为本人的不审慎,导致对其理解出了偏差,还麻烦了社区看我的代码(悲)只能说个人出道的开源社区还是需要对自己的所有笔代码负责。
+++pr:
++
涉及到的各种硬件手册:
++
这也是我最后这一周在做的工作,虽说只有短短一周,但是每天都研究这个花了我不少时间和精力()目前算是写完了它的所有功能(大概),并且已经能把LED闪烁和中断绑定函数润起来了,提的pr在这里。
+以前对驱动的理解,还停留在手把手教你做事的xv6的netlab。也因而,这次可以算是以完全一无所知的状态接下了这个任务。
+不过,好在有以前那个短暂的lab经验,我还是稳扎稳打地定下了具体的学习步骤:调研(包括获取各种data book、schematic、rtt官方文档、Linux和rtt相关代码),然后就是学习。
+好在有设备树的研究积淀,我也算是比较快地掌握了milkv上gpio的分布、型号及其地址空间,从而顺藤摸瓜找到了对应gpio型号的Linux驱动代码参考和硬件手册,算是免去了不少麻烦。
+然后,我观察rtt的gpio驱动们,也找到了对应的pin.md文档,了解了下大致的代码框架思路:
+于是接下来的工作也可以比较独立地划分为两部分,一个是数据读写的实现(通过LED闪烁程序测试),另一个是rtt特有的中断回调函数的支持(通过中断程序测试),可以专注对这两个方面开发了。
+其中,数据读写的实现需要对寄存器和引脚号等有所了解。寄存器相对比较简单,只需阅读dwapb的data book即可;而引脚号到gpio的转换则花了我不少时间,做了许多猜想并且进行验证,最后误打误撞地“猜”中了正确思路,通过了LED测试。
+不过引脚号我现在还是不大懂,总之先参照别的bsp写法自己编了个,等着代码review看下吧。
+前期调研一直到LED亮起来花了我整整五六天()相比于此的困难,中断倒显得简单了许多,毕竟它属于是偏软件相关的。调了一天,也从Linux和其他bsp那边抄了些代码,最终在凌晨两点半成功完成了功能测试()今天又花了一个下午整理了下代码和写日记,最终总算是把这个作业交上去了。
+整个过程光是写看起来还是比较轻松,但是由于初次开发摸索,每个小跨越都得花费我不少时间去调研搜索,经常是在长达几个小时的不知所措后才短暂地获得了一些光明,我甚至多次想过要不要去辞职了(((。总之,最后还是坚持了下来。看到蓝色的LED在夜晚的T5中闪烁,我还是十分激动的,眼泪都爆出来了()
+哎,坚持难能可贵,很高兴我最终做到了,虽然尚有测试上的不完善,以及还在等待review。
+总之,这一个月来我学到了许多,同时也对我以前未曾涉足的空白领域做了许多探索,包括对设备树、对嵌入式开发、对Linux设备驱动等的学习,总体来说还是十分甚至九分地开心。希望下个月能再接再厉。
+]]>http://localhost/webdemo4_war/*.do
。
]]>
将主存储器以及各种外设接口卡里面内置的存储器连接起来,就形成了内存地址空间。内存地址空间中的地址是真实的物理地址。RISC-V架构的指令使用的地址是虚拟地址。为了通过指令中的虚拟地址访问到真实的物理内存,需要进行从虚拟地址到物理地址的转换。从虚拟地址到物理地址的转换,就需要通过页表来实现。
-在RISC-V指令集中,当我们需要开启页表服务时,我们需要将我们预先配置好的页表首地址放入 satp
寄存器中。从此之后, 计算机硬件 将把访存的地址 均视为虚拟地址 ,都需要通过硬件查询页表,将其 翻译成为物理地址 ,然后将其作为地址发送给内存进行访存。
xv6采用的指令集标准为RISC-V标准,其中页表的标准为SV39标准,也就是虚拟地址最多为39位。
-虚实地址翻译流程:
+traps=系统调用+异常+中断。本章着重讲traps概述以及traps中的系统调用。
+对trap的处理包含四个部分:硬件处理、中断向量、trap handler、对应的处理函数
++++
+
在RISC-V中,异常通常是由于程序执行过程中的错误或非预期事件而引起的,包括故障(faults)、陷阱(traps)和中止(aborts)。中断(interrupts)则是由外部事件触发的,例如定时器到期、外部设备请求等。中断是异步事件,与当前正在执行的指令无关,因此会在任何时候发生。
+xv6是基于RISC-V架构的。因此,发生异常的时候,就会跳转到统一的kernel trap,然后再在里面通过读取scause来进行相应处理。
+发生中断的处理方式就和x86差不多了,都是通过中断向量实现的。
+
risc-v为trap提供了一组寄存器:
satp
中。satp
找到根页表的物理页帧号,转成物理地址(Offset
为0),通过虚拟地址的L2
索引,找到对应的页表项。Offset
为0),通过虚拟地址的L1
索引,找到对应的页表项。Offset
为0),通过虚拟地址的L0
索引,找到对应的页表项。Offset
,转成物理地址(Offset
和虚拟地址Offset
相同)。stvec
+trap handler的入口地址
+sepc
+原程序PC
+scause
+中断号
+sscratch
+TRAPFRAME地址
+sstatus
+是否允许中断,以及中断来自内核态还是用户态
+页表由页表项PTE(Page Table Entries)构成,每个页表项由44位的PPN(Physical Page Number)和一些参数flag组成。
---Each PTE contains flflag bits that tell the paging hardware how the associated virtual address is allowed to be used. PTE_V indicates whether the PTE is present: if it is not set, a reference to the page causes an exception (i.e. is not allowed). PTE_R controls whether instructions are allowed to read to the page. PTE_W controls whether instructions are allowed to write to the page. PTE_X controls whether the CPU may interpret the content of the page as instructions and execute them. PTE_U controls whether instructions in user mode are allowed to access the page; if PTE_U is not set, the PTE can be used only in supervisor mode.
-这个表项的几个参数定义在kernel/riscv.h中的341行左右。
+The above registers relate to traps handled in supervisor mode, and they cannot be read or written in user mode.
+There is an equivalent set of control registers for traps handled in machine mode; xv6 uses them only for the special case of timer interrupts.
虚拟地址有64bit,其中25bits未使用,39bits包含了27位的PTE索引号以及12位的offset。
-物理地址有56位,由PPN和offset拼接组成。
-以单页表为例,物理地址形成过程如下图所示。
-每个页表项PTE索引着一页。因而,每一页的大小为2^12=4096B。单页表中PTE的索引号有2^27个,因而单页表中表项有134217728个,即可以代表134217728页。页表实际上也是以页的形式存储的。因而单页表需要的存储空间为(2^27x7)/2^12=2^15x7=229376页。
-RISC-V架构中真实情况是会有三级页表。三级页表结构相比于单级页表结构,会占据更多的物理存储空间。
-每个页表项PTE索引着一页,这一页可能代表着另一个页表,也可能代表着内存中需要的指令和数据。因而,每一页的大小为2^12=4096B。三页表中,一级页表中PTE的索引号有512个,可以代表的物理内存页数有512x515x512=2^27页,即可以代表134217728页。页表实际上也是以页的形式存储的,一个页表有2^9x7个字节,可以存储在1页中。因而三页表需要的存储空间为1+2^9+2^18 = 262657页。
-三级页表结构相比于单级页表结构,可以节省更多内存空间。
+每个CPU都有自己的一套这样的控制寄存器。
+时钟中断、device interrupt以及关中断的情况下,不会做以下步骤。
- +-\1. If the trap is a device interrupt, and the sstatus SIE bit is clear, don’t do any of the following.
+\2. Disable interrupts by clearing SIE.关中断
+\3. Copy the pc to sepc.保存PC
+\4. Save the current mode (user or supervisor) in the SPP bit in sstatus.保存mode
+\5. Set scause to reflflect the trap’s cause.保存中断号
+\6. Set the mode to supervisor.切换到内核态
+\7. Copy stvec to the pc.将trap handler写入pc,开始执行trap handler【uservec or kernelvec?】
考虑到这样一个进程:
-进程使用页表时,需要将整个页表读入内存。
-如果使用单级页表,尽管一个进程仅使用到页表中的某两项,也需要把整个页表都读入内存,光是页表就占据了2^15x7x4k/2^20 约为1G的内存空间。
-如果使用三级页表,一个进程需要用到某两页。假设这两页存储在不同的二级页表中,则只需要读入1+2+2=5页 约为20K的内存空间。
-两者相对比,显然用三级页表比单级页表顶多了。三级页表相较于一级页表,多用了13%的物理空间,却可以节省99.998%的空间。
-每个进程会保留自己的一份用户级别的页表地址。当轮到自己使用CPU时,会将CPU的satp寄存器更换为自己的页表地址。
-介绍了xv6中内核的页表结构。
+切换到内核页表、切换内核栈、保存寄存器现场这些工作交给操作系统完成。
+从用户态来的trap会经历怎么样的过程?
+前面说到,下面需要进行页表的切换,页表的切换必然是接下来要做的指令的某个环节。那么为了让页表切换之后,CPU还知道要从哪里取指执行,就要让某段物理内存在内核空间和用户空间的虚拟地址一样。这样,不论页表是用户的还是内核的,都可以通过同样的虚拟地址访问到该段存放指令的物理内存从而继续执行。
+这段虚拟地址就是trampoline。它在内核页表和用户页表都位于MAXVA的位置。
--这里为了方便,就把三级页表省略了,只留下va和pa的对比
+我感觉这段大概可以这么理解:
+通过查看代码,可知trampoline段实际上存储的是trampoline.S中的数据,也即uservec和userret的汇编代码,也即执行切换页表我们实际上就是在执行trampoline里的代码。trampoline的存在,就可以使得每个页表的这部分都是这两个的代码,这样一来切换页表也就不影响指令流的执行。
每个进程都有一个用户级别的页表。xv6给内核提供了一个单独的内核地址空间的页表。其层级映射关系如下:
在kernel/memlayout.h中正记录了这些参数:
-// Physical memory layout |
stevc存储的正是trampoline段中的uservec。
+sscratch里面存的是trapframe的值。
+trapframe存在于用户空间中,并且每个进程的trapframe所处位置固定是在trampoline下方。
+首先将寄存器的值都存入trapframe中;然后,再从trapframe中读取内核栈指针、当前CPUid,下一步要跳转的usertrap的地址,以及内核页表。最后,uservec切换到内核页表,并且jmp到usertrap。
+#in kernel/trampoline.S |
由图可知,一直从0x0到0x86400000,都是采取的直接映射的方式,虚拟地址=物理地址,这段是内核使用的空间。在0x0-0x800000000阶段,物理地址代表着各种IO设备的存储器。
-但是注意,在0x86400000(PHYSTOP)以上的地址都不是直接映射,这些非直接映射的层级包含两类:
-trampoline
---It is mapped at the top of the virtual address space; user page tables have this same mapping.
-
它有一点很特殊的是,它实际对应的物理内存是0x80000000开始的一段。也就是说,0x80000000开始的这段内存,既被直接映射了,也被trampoline通过虚拟地址映射了。它被映射了两次。
-内核栈
--Each process has its own kernel stack, which is mapped high so that below it xv6 can leave an unmapped guard page. The guard page’s PTE is invalid (i.e., PTE_V is not set), so that if the kernel overflflows a kernel stack, it will likely cause an exception and the kernel will panic.
-guard page可以用来防止内核栈溢出。
+注:trampoline和trapframe有一些相通点。
+trampoline为了保障某段物理内存的虚拟地址在内核栈和用户栈中不变,做出的努力是,在内核栈和用户栈都分配同一位置的PTE。
+trapframe用于保护现场、用户态向内核态传递参数等等,做出的努力是,在用户栈分配同一位置的PTE,在内核态的局部变量中保存了自己的物理地址。
+这两个说实话有点容易混起来,因为我想了半天trampoline可不可以用类似trapframe一样的方法,结论是不行。因为你trampoline的作用是维持指令序列依然不变,不会突然没掉;而trapframe段是用来存储数据而非执行的,对其的控制也是需要指令的。如果trampoline使用第二种方法,指令流就会断掉,更别说别的了。
内核使用PTE_R和PTE_X权限映射trampoline和kernel text。这表明这份内存段可以读,可以被当做指令块执行,但不能写。其他的块都是可读可写的,除了guard page被设置为不可访问。
-操作地址空间和页表部分的代码都在kernel/vm.c
中。代表页表的数据结构是pagetable_t
。
vm.c的主要函数有walk、mappages等。walk用来在三级页表中找到某个虚拟地址表项,或者创建一个新的表项。mappages用来新建一个表项,主要用到了walk函数。
-vm.c中,以kvm开头的代表操纵内核页表,以uvm开头的代表操纵进程里的用户页表。
-一开始操作系统初始化时,会调用vm.c中的kvminit来创建内核页表。主要就是在以内核地址空间的页表结构在填写页表。
-void |
作用是得到trap发生的原因,并且执行对应的处理程序,然后返回结果。
+// handle an interrupt, exception, or system call from user space. |
其中,kvmmap用来在内核页表中添加一个新的表项。其函数形式为
-// add a mapping to the kernel page table. |
比如说system call会修改trapframe中的a0为返回的结果,会获取trapframe中的各个参数。这个“保护现场“感觉是非常微妙的,它兼顾了保护现场和传递参数两个作用
+回到用户态。之前陷入内核态对stvec、satp、sp、hartid、trap handler都做了适应内核态的改变,因而这里就需要改回原来适应用户态的样子,然后返回用户态。
+// return to user space |
实现主要逻辑的是mappages函数
-// Create PTEs for virtual addresses starting at va that refer to |
.globl userret |
通过虚拟地址获取表项主要是通过walk实现的
-// Return the address of the PTE in page table pagetable |
++Chapter 2 ended with initcode.S invoking the exec system call (user/initcode.S:11). Let’s look at how the user call makes its way to the exec system call’s implementation in the kernel.要讲如何从用户态找到exec的代码了。
+
讲的是系统调用时,是如何把用户态传递的地址转化为内核态地址的。
+这个部分可以看看hit实验的实验3 6.7,讲得很详细,而且流程是差不多的。linux0.11的get_fs_byte()
就相当于xv6的copyinstr
。
不同于用户态还得先潜入内核再潜出内核,内核的trap可简单多了,省去了切来切去各种东西的步骤,只需当做一个普通的函数调用就行。
+# in kernel/kernelvec.S |
使用的是kvminithart函数。它将内核页表的root page table的物理地址写入了satp寄存器。从这个函数之后,就开启了内存映射。
-// Switch h/w page table register to the kernel's page table, |
++kerneltrap is prepared for two types of traps: device interrrupts and exceptions.
+It calls devintr (kernel/trap.c:177) to check for and handle the former. If the trap isn’t a device interrupt, it must be an exception, and that is always a fatal error if it occurs in the xv6 kernel; the kernel calls panic and stops executing.
+If kerneltrap was called due to a timer interrupt, and a process’s kernel thread is running (rather than a scheduler thread), kerneltrap calls yield to give other threads a chance to run.
+
// in kernel/trap.c |
其中sfence_vma()的用途是强制更新TLB的旧页表,类似于Java volatile的作用。
-附上书里的详细解释:
-TLB与页表类似于cache与主存的关系。TLB保存了页表的一部分。
-我怎么感觉怪怪的啊?因为TLB既然是高速缓存,那么读写页表也应该优先从TLB读写【注:应该就是从这里开始错的hhh写应该是直接写入页表】。所以说,会陈旧的应该是主存中的页表,而不是TLB中的页表。但是,书里是说,改完页表必须通知TLB更改。也就是说,读写页表不是从TLB读写的,那该是从哪里?是TLB以外的free memory吗?
-不过,要是从多CPU的角度思考,说不定他这个意思是某个CPU的TLB变了,需要通知其他所有CPU的TLB也变。虽然不同CPU当前执行的进程是不一样的,使用的页表项不一样,切换进程的时候也会把用户地址空间的页表项flush掉。但是内核地址空间的页表项一般是不会随着进程切换而flush掉的。所以内核页表修改就需要手动多CPU同步。
-我认为多CPU角度考虑更加合理,因为它最后说了,xv6会在内核页表init后flush,以及在从内核态切换回用户态的时候flush。这两个(好像)都影响内核页表比较多,所以就需要手动flush一下。
-之后学了缺页异常后,可以发现这里其实是没问题的。
- -页表的管理(创建、更新、删除等)是由操作系统负责的。地址转换时,页表检索是由硬件内存管理单元(Memory Management Unit, MMU)负责的。MMU通常由两部分构成:表查找单元(Table Walk Unit, TWU)和转换旁路缓冲(Translation Lookaside Buffer, TLB)[2]。TWU负责链式的访问PDE、PTE,完成上述的查表过程。
-应用多级页表之后,想要完成一次地址转换,需要访问多级目录和页表,这么多次的内存访问会严重降低性能。
-为了优化地址转换速度,人们在MMU中增加了一块高速cache,专门用来缓存虚拟地址到物理地址的映射,这块cache就是TLB[7][8]。MMU在做地址转换的时候,会先检索TLB,如果命中则直接返回对应的物理地址,如果不命中则会调用TWU查找页表。
-TLB中缓存的是虚拟地址到物理地址映射。然而,多级页表的查找是一个链式的过程,对于在虚拟地址空间中连续的两个页,它们的各级目录项可能都是一样的,只有最后一级页号不一样。查找完第一个虚拟页之后,我们可以将相同的前级目录项都缓存起来。查找第二个虚拟页时,可以直接使用缓存好的前几级目录项,节省查找时间。这种缓存叫做Page Structure Cache[9]。
-而当TLB和MMU中都没有该物理页,就会发生缺页异常。但是操作系统仅会对页表更新,而不会被TLB更新。故而,TBL中数据可能陈旧,需要手动flush。
-在内核运行的时候,需要申请很多空间用来存放各种数据。
--The kernel must allocate and free physical memory at run-time for page tables, user memory, kernel stacks, and pipe buffers.
+Page-fault exceptions
似乎xv6是没有这个缺页exception的。这里主要讲解了三个可以利用缺页中断实现的优化:COW fork、lazy allocation、paging from disk。还提及了automatically extending stacks 以及memory-mapped fifiles。
+Lab:Trap
+-This lab explores how system calls are implemented using traps. You will first do a warm-up exercises with stacks and then you will implement an example of user-level trap handling.
用的是这段空闲内存:
--
-It keeps track of which pages are free by threading a linked list through the pages themselves.
+RISC-V assembly
题目和答案
++参考:
+ +There is a file
+user/call.c
in your xv6 repo. make fs.img compiles it and also produces a readable assembly version of the program inuser/call.asm
.Read the code in call.asm for the functions
+g
,f
, andmain
. Here are some questions that you should answer:+
+- +
a2
+- +
被inline掉了
+- +
0x64A
++
auipc的作用是把立即数左移12位,低12位补0,和pc相加赋给指定寄存器。这里立即数是0,指定寄存器是ra,即ra=pc=0x30=48。jalr作用是跳转到立即数+指定寄存器处并且把ra的值置为下一条指令。因此jalr会跳转1562+48=1594=0x64A处,观察汇编代码可知确实在000000000000064a处。
+- +
0x38
+- +
++Run the following code.
++ +
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);What is the output? Here’s an ASCII table that maps bytes to characters.
+The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set
+i
to in order to yield the same output? Would you need to change57616
to a different value?+
- +
取决于寄存器a2(第3个参数)的值。
+Backtrace
+-For debugging it is often useful to have a backtrace: a list of the function calls on the stack above the point at which the error occurred.
++
The compiler puts in each stack frame a frame pointer that holds the address of the caller’s frame pointer. Your
backtrace
should use these frame pointers to walk up the stack and print the saved return address in each stack frame.kalloc.c中就是这么实现的。
-Code: Physical memory allocator
内核运行时申请释放空闲物理空间是通过
kernel/kalloc.c
完成的。它为内核栈、用户进程、页表和管道buffer服务。--kalloc.c用来在运行时申请分配新的一页,上面的vm.c正是用了kalloc申请一页,要么作为页表,要么作为存储数据的第三级页表指向的物理内存。
+Some hints:
++
- +
Add the prototype for backtrace to
+kernel/defs.h
so that you can invokebacktrace
insys_sleep
.- +
The GCC compiler stores the frame pointer of the currently executing function in the register s0. Add the following function to kernel/riscv.h:
++ +
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}and call this function in backtrace to read the current frame pointer. This function uses in-line assembly to read s0.
+- +
These lecture notes have a picture of the layout of stack frames. Note that the return address lives at a fixed offset (-8) from the frame pointer of a stackframe, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.
+- +
Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address. You can compute the top and bottom address of the stack page by using
+PGROUNDDOWN(fp)
andPGROUNDUP(fp)
(seekernel/riscv.h
. These number are helpful forbacktrace
to terminate its loop.最后应该会在空闲内存内形成这样的结构:
-内存分成一页一页的,每页内存中的前几个字节存储着其对应队列中下一块内存的物理地址。不一定是从小地址到大地址顺序连接。
+感想
我超,这题真的是那怎能只叫一个拷打……
+存储在s0中的栈帧指针
这个应该是risc-v的约定成俗的特性。我搜了一下risc-v的栈帧指针保存在哪个寄存器,看到了这样一篇文章:
--It store each free page’s run structure in the free page itself, since there’s nothing else stored there.
+ +
+
// Physical memory allocator, for user processes,
// kernel stacks, page-table pages,
// and pipe buffers. Allocates whole 4096-byte pages.
// 释放在这范围内的物理内存空间
void freerange(void *pa_start, void *pa_end);
// 也就是上面说的free memory的起始位置
extern char end[]; // first address after kernel.
// defined by kernel.ld.
// run代表的是一页内存
struct run {
struct run *next;
};
// 代表了整个内核空闲的物理空间
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
void
kinit()
{
initlock(&kmem.lock, "kmem");
// init的时候先清空空闲空间,建立空闲页队列
freerange(end, (void*)PHYSTOP);
}
void
freerange(void *pa_start, void *pa_end)
{
char *p;
// PGROUNDUP和PGROUNDDOWN是用于将地址四舍五入到PGSIZE
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
// Free the page of physical memory pointed at by pa,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
struct run *r;
// pa得是整数页,并且得在内核物理内存范围之间
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
// 之后将在pa对应的那一页的前几个字节写入next字段
r = (struct run*)pa;
// 这意思就是在空闲内存的链表队列中新增一块
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}这个信息没有放在题干提示,是在考察信息检索能力吗(
+栈的结构与栈帧的理解
+
这是来自hint的栈结构。整个栈存储在一页中,由高地址向低地址增长。栈帧代表了一次函数调用,其中会存储如函数名、函数参数、局部变量等等信息。有几次函数调用就有几个栈帧,栈由栈帧组成。
+s0中存储的栈帧指针fp指向的是栈帧的最高地址,如图fp所示。
++我理解错了栈帧的定义,都怪我基础不大牢固也不认真思考【悲】我一开始以为stack frame指的是一个栈,也即一页空间【我知道栈帧这个中文名词,但遇到英语就短路了】。老师画的这个图也被我理解为多个栈,也即多页拼在一起,要打印的Return Address处于页的最顶部。我就在这个思路上一去不复返了,压根没有意识到一个进程只有一个栈【大悲】然后顺带脑补把r_fp()也曲解了,以为它的意思是读取当前栈【非常自然地认为有很多个栈←】的下一个栈的最低地址【因为栈换掉了,所以s0也会变成父亲的栈的地址】。于是就写出了这样的代码:
+-
void
backtrace(){
printf("backtrace:\n");
uint64 kstack = PGROUNDUP((uint64)(myproc()->kstack)+1);
uint64 nstack = 0;
while((nstack=(uint64)r_fp())!=0){
printf("%p\n",*((uint64*)(kstack-8)));
kstack = nstack;
}
}Process address space
当用户进程叫xv6分配内存时,xv6会用kalloc去取,然后登记在页表上。
---The stack is a single page, and is shown with the initial contents as created by exec. Strings containing the command-line arguments, as well as an array of pointers to them, are at the very top of the stack. Just under that are values that allow a program to start at main as if the function main(argc, argv) had just been called.
+结果最后死循环了。去看了别人的代码发现他们写的结构就跟我完全不一样。琢磨着画着图,最后找了stack frame的定义,才恍然大悟(
-
Code: sbrk
-Sbrk is the system call for a process to shrink or grow its memory. The system call is implemented by the function growproc (kernel/proc.c:239).
+思路形成
我们只需遍历栈中所有栈帧,打印每个栈帧的Return Address部分就行。通过r_fp()获取第一个栈帧的位置,其他栈帧的位置由Prev.Frame获取。循环的界限是PGROUNDUP(r_fp()),因为栈只有一页的空间。
+代码
+ +
void
backtrace(){
printf("backtrace:\n");
uint64 stack = r_fp();
uint64 nstack = 0;
uint64 top = PGROUNDUP(stack);
while(stack!=top){
nstack=*((uint64*)(stack-16));
printf("%p\n",*((uint64*)(stack-8)));
stack = nstack;
}
}Alarm
+-In this exercise you’ll add a feature to xv6 that periodically alerts a process as it uses CPU time. This might be useful for compute-bound processes that want to limit how much CPU time they chew up, or for processes that want to compute but also want to take some periodic action.
+More generally, you’ll be implementing a primitive form of user-level interrupt/fault handlers; you could use something similar to handle page faults in the application, for example.
+You should add a new
sigalarm(interval, handler)
system call. If an application callssigalarm(n, fn)
, then after everyn
“ticks” of CPU time that the program consumes, the kernel should cause application functionfn
to be called. Whenfn
returns, the application should resume where it left off.+
// Grow or shrink user memory by n bytes.注意单位是bytes,grow n+,shrink n-
// Return 0 on success, -1 on failure.
// 主要逻辑还是通过vm.c实现
int
growproc(int n)
{
uint64 sz;//size
struct proc *p = myproc();
sz = p->sz;
if(n > 0){
if((sz = uvmalloc(p->pagetable, sz, sz + n, PTE_W)) == 0) {
return -1;
}
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
}
p->sz = sz;
return 0;
}感觉从alarm中可以窥见信号的实现思路:
++
而alarm的机理感觉也有点类似。用户先通过sigalarm注册定时函数,内核在时钟中断的时候对该标记位进行检查,然后去do_signal回到用户态执行用户的signal handler,再通过sigreturn回到用户模式。sigalarm相当于一个信号注册函数,sigreturn也就是上图的sigreturn。
+分析
初见思路
思路:sigalarm需要在用户程序在用户态运行的情况下,监测到用户程序已经运行了n个时间片,然后发出中断请求。我们会新设置一个中断类型alarm。kerneltrap接收到sigalarm的中断请求,检测到中断类型为alarm,就会在处理的时候调用fn。fn调用完就自然而然利用中断恢复到原来的现场了。所以要做的可以分为两部分。但问题是,如何让sigalarm在用户程序运行的同时监测n个时间片呢?难道得fork一个新的进程吗?然后父进程返回,子进程执行类似sleep里面那样的监测,直到时间片到了,就发送一个中断请求,让父进程停止,执行完fn回来之后就exit。
+正确思路
可以看到,初见思路很多地方跟最后是不一样的。其中错得最离谱的,也是比较隐坑很容易因为想不明白就寄了的,是handler是个用户态的函数(。你不可能在内核态中调用fn,然后fn执行完后再自然而然地通过中断机制返回,因为你想要执行fn就必须进入用户态。这一点是需要一开始明确的。
+明确了这一点后,让人更加不知道该怎么办了。那就一步步跟着指导书的脚步来思考吧。
+part 1
首先,明确你需要实现什么。你需要实现两个系统调用,一个是sigalarm,一个是sigreturn。结合提示,可知实验设计者给我们的思路是,通过sigalarm设置定时函数,通过sigalarm(0,0)取消定时函数。每次时钟中断检测当前定时时间是否达到,若已达到,则跳到定时函数执行。定时函数执行完后,需要借助sigreturn,才能正确返回时钟中断前的程序点。
+part 2
这又可拆解为几个要点:
++
+- 如何实现“定时”?
+- 时钟中断在内核态的usertrap被检测。怎么从usertrap出来跳到定时函数而非原程序执行点?
+- 执行完定时函数后,怎么样才能回到原程序执行点?
+part 3
一个个来说,首先是如何实现定时。这个很简单。参照sys_sleep的代码:
+-
uint64
sys_sleep(void)
{
int n;
uint ticks0;
backtrace();
if(argint(0, &n) < 0)
return -1;
acquire(&tickslock);
ticks0 = ticks;
while(ticks - ticks0 < n){
if(myproc()->killed){
release(&tickslock);
return -1;
}
sleep(&ticks, &tickslock);
}
release(&tickslock);
return 0;
}+
// Allocate PTEs and physical memory to grow process from oldsz to
// newsz, which need not be page aligned.不需要页对齐 Returns new size or 0 on error.
uint64
uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm)
{
char *mem;
uint64 a;
if(newsz < oldsz)
return oldsz;
// oldsz向上取整
oldsz = PGROUNDUP(oldsz);
// 每页alloc
for(a = oldsz; a < newsz; a += PGSIZE){
mem = kalloc();
if(mem == 0){
// 说明失败,恢复到原状
// 这里不用像下面一样kfree是因为这里压根没有alloc成功
uvmdealloc(pagetable, a, oldsz);
return 0;
}
// 除去junk data
memset(mem, 0, PGSIZE);
// 放入页表
if(mappages(pagetable, a, PGSIZE, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
// 不成功
// dealloc原理是顺着页表一个个free的。由于mem此处没有成功放入页表,所以就得单独free掉
kfree(mem);
uvmdealloc(pagetable, a, oldsz);
return 0;
}
}
return newsz;
}可知,我们可以用ticks表示当前系统滴答数。这样,我们就可以在proc域里维护一个变量lasttick,记录上一次执行handler时的滴答数。每次在时钟中断时检测,所以需要写在
+kernel/trap.c
中的usertrap
中。part 4
然后,是怎么从usertrap出来跳到指定程序结束点。在书中,我们知道,sepc寄存器保存了中断前原程序的下一个执行点,sepc的备份存储在了proc域中的栈帧中。当中断返回时(在usertrapret中),会从栈帧中的epc字段读取sepc的备份赋值给sepc,再由sret帮助我们跳转到原程序点。因而,如果想要改变跳转点,我们只需要修改
+p->trapframe->epc
就行。part 5
最后,是如何从periodic回到原程序执行点。
++
这是每次进行时钟中断时的栈情况和执行代码链:t1->trampoline->usertrap->handler。
+再然后,handler调用了sigreturn,用户栈中就会产生sigreturn的栈帧:
++
此时,如果sigreturn执行完,就会在这样的情况下执行handler的ret指令:
++
ret指令会把栈帧弄走,也就是说会直接回到某个错误的地方去。这显然不大合适。所以,我们要做的,就是在sigreturn之后,不执行handler的ret指令,也不执行sigreturn的ret指令,而是直接恢复到时钟中断前的上下文。时钟中断前的上下文,会因在handler中调用sigreturn系统调用,而被覆盖,因而,我们就需要记录时钟中断前的上下文,也即在proc域中保存trapframe的一份拷贝savedtrap,每次时钟中断都更新一次savedtrap,然后在sigreturn调用的时候将proc原本的trapframe替换为savedtrap即可。这样一来,就完成了这道题。
+感想
这题目确实最终代码看起来完全不难,但是非常地拷打。。。。我前前后后修修补补差不多一共花了五个小时之久。
+计时怎么计,以及使用trapframe->epc来跳转这两点还是很容易想到的【话虽如此,其实也很曲折】。主要难点还是在怎么恢复现场。怎么说呢,我花了这么久做实验,但是实际笔记却写不出个鬼来,足以看出其复杂程度。
+我主要还是思维固化了点,一直在想,怎么确保它正确返回现场。我一开始以为proc域保存一个寄存器状态,且只用在一开始设置定时函数也即sigalarm的时候保存一次就行了,并且认为其是epc。然而实际操作后发现usertrap崩了,并且epc中存储的并不是程序被时钟中断的地方,而是各种神奇的地方,具体我也忘了,反正不能行。我印象最深刻的是有一次停在了usys.S中的sigreturn的最后一个ret处。我就在想,也许是栈出了问题。于是我就想着直接在sigreturn的时候把epc指向栈帧中的return address,直接回到原执行段。我百度了一下,确实有这么个寄存器ra,存储着return address。于是我就把proc域的状态换成了ra,依然仅保存一次,最后发现还是不行,程序在test0之后就异常终止了,main也回不去,十分古怪,十分匪夷所思。我实在没忍住,百度了一下大家怎么做的,发现大家压根没有我这样的二选一的烦恼,是直接保存整个栈帧。而且也不是仅保存一次,而是每次时钟中断触发都保存一次。我觉得十分奇怪震惊,但此时已是差不多晚饭时间,我就先去吃了个饭()
+回来之后,我细细画了图【向正确思路part5中的那样】,发现我原来那个只保存两者之一,且都只保存一次的方式,确实完全不能行。但是,我发现两个一起保存,并且每次时钟中断保存的方法,似乎能行,而且,比保存一整个栈帧要聪明得多。于是我就去试了,发现还是不行。我再细想了一遍,发现,如果想回去原程序的现场,除了ra和epc,还有一个很重要的东西需要保存,那就是——用户栈指针sp!
+也就是说,只需保存ra、epc、sp,就可以保证回到正确的时钟中断前的位置:
++
此为handler中sigreturn执行完要返回时的状态。
+当处在handler中时,sp的值为sigreturn处的栈帧。执行系统调用时,proc域中的上下文被覆盖,也即时钟中断前的上下文被覆盖。如果此时不对栈帧中的sp进行恢复,仅恢复ra和epc,在从sigreturn返回到epc对应处也即t1,t1执行ret想回到main的时候,就会回不去,而是回到了sigreturn要回的位置,也即handler的位置,然后不知不觉就寄了。所以,就需要防止sp被覆盖。因而,再保存一个状态sp,就可以保障回到正确的地方了。测试出来,kernel确实不再会panic了。
+但是由于运行时很多除这三个以外的寄存器都被改过了,回是回得去,接下来干的活就不一定对了。因此为了保险以及通用性以及便利性来看,还是像别人那样直接保存栈帧比较ok。
+还有一件事,就是上述错误中经常会出现的一个输出结果:
+-
usertrap:unexpected cause scause = 0x0cCode:exec
-Exec is the system call that creates the user part of an address space. It initializes the user part of an address space from a fifile stored in the fifile system.
-+
exec
是创建地址空间的用户部分的系统调用。它使用一个存储在文件系统中的文件初始化地址空间的用户部分。我留意了一下是什么意思。网上搜索得,scause=12,说明这是一个instruction page fault,而这个缺页错误说明了什么?:
++
这样,一切都明朗了。出现了scause=0x0c的意思就是说pc里的值不恰当,也就是说上面错误的方法都会跳转到错误的地方去。
+Lab:xv6 lazy page allocation
+-参考:https://blog.csdn.net/m0_53157173/article/details/131349366
- -
int
exec(char *path, char **argv)
{
char *s, *last;
int i, off;
uint64 argc, sz = 0, sp, ustack[MAXARG], stackbase;
struct elfhdr elf;
struct inode *ip;
struct proghdr ph;
pagetable_t pagetable = 0, oldpagetable;
struct proc *p = myproc();
//开始打开文件的意思吧(
begin_op();
//ip是一个inode
//打开路径为path的文件
if((ip = namei(path)) == 0){
end_op();
return -1;
}
//暂时锁住文件,别人不许动
ilock(ip);
//之后应该就是把文件读入内存吧
// Check ELF header
if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf))
goto bad;
if(elf.magic != ELF_MAGIC)
goto bad;
//分配新页表
if((pagetable = proc_pagetable(p)) == 0)
goto bad;
//elfhd应该指的是可执行文件头
// Load program into memory.
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
goto bad;
if(ph.type != ELF_PROG_LOAD)
continue;
if(ph.memsz < ph.filesz)
goto bad;
if(ph.vaddr + ph.memsz < ph.vaddr)
goto bad;
if(ph.vaddr % PGSIZE != 0)
goto bad;
//总之顺利读到了
uint64 sz1;
//读到了就给它分配新空间并且填入页表
if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0)
goto bad;
sz = sz1;
if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
goto bad;
}
//在这里解锁
iunlockput(ip);
end_op();
ip = 0;
p = myproc();
uint64 oldsz = p->sz;
//读完文件,开始造一个新的用户栈【fork之后用户栈是不会清空的】
// Allocate two pages at the next page boundary.
// Make the first inaccessible as a stack guard.
// Use the second as the user stack.
sz = PGROUNDUP(sz);
uint64 sz1;
if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE, PTE_W)) == 0)
goto bad;
sz = sz1;
// mark a PTE invalid for user access.造guard page
uvmclear(pagetable, sz-2*PGSIZE);
// sp为栈顶
sp = sz;
// 应该指的是栈尾
stackbase = sp - PGSIZE;
// 开始往栈中填入执行参数
// Push argument strings, prepare rest of stack in ustack.
for(argc = 0; argv[argc]; argc++) {
if(argc >= MAXARG)
goto bad;
sp -= strlen(argv[argc]) + 1;
sp -= sp % 16; // riscv sp must be 16-byte aligned
if(sp < stackbase)
goto bad;
//argv来自用户空间,所以需要使用copyout
if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
goto bad;
//这什么东西
//exec一次将参数中的一个字符串复制到栈顶,并在ustack中记录指向它们的指针
ustack[argc] = sp;
}
//放置空指针
ustack[argc] = 0;
// push the array of argv[] pointers.
sp -= (argc+1) * sizeof(uint64);
sp -= sp % 16;
if(sp < stackbase)
goto bad;
if(copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)) < 0)
goto bad;
// arguments to user main(argc, argv)
// argc is returned via the system call return
// value, which goes in a0.
p->trapframe->a1 = sp;
// Save program name for debugging.
for(last=s=path; *s; s++)
if(*s == '/')
last = s+1;
safestrcpy(p->name, last, sizeof(p->name));
//只有成功了才会来到这,才会覆盖掉旧的内存镜像
// Commit to the user image.
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
p->trapframe->epc = elf.entry; // initial program counter = main
p->trapframe->sp = sp; // initial stack pointer
proc_freepagetable(oldpagetable, oldsz);
return argc; // this ends up in a0, the first argument to main(argc, argv)
bad:
//释放新镜像,不改变旧镜像
if(pagetable)
proc_freepagetable(pagetable, sz);
if(ip){
iunlockput(ip);
end_op();
}
return -1;
}Real world
-
xv6内核缺少一个类似
-malloc
可以为小对象提供内存的分配器,这使得内核无法使用需要动态分配的复杂数据结构。【确实,感觉一分配就是一页(】内存分配是一个长期的热门话题,基本问题是有效使用有限的内存并为将来的未知请求做好准备。今天,人们更关心速度而不是空间效率。此外,一个更复杂的内核可能会分配许多不同大小的小块,而不是(如xv6中)只有4096字节的块;一个真正的内核分配器需要处理小分配和大分配。
-Lab:Pagetable
-In this lab you will explore page tables and modify them to to speed up certain system calls and to detect which pages have been accessed.
++-来自书本:
+Another widely-used feature is called lazy allocation, which has two parts:
++
+- First, when an application calls sbrk, the kernel grows the address space, but marks the new addresses as not valid in the page table.
+- Second, on a page fault on one of those new addresses, the kernel allocates physical memory and maps it into the page table.
+The kernel allocates memory only when the application actually uses it.
不过遗憾的是usertests还有好几个没通过,具体都标注了。
-Speed up system calls
-When each process is created, map one read-only page at USYSCALL (a VA defined in
-memlayout.h
). At the start of this page, store astruct usyscall
(also defined inmemlayout.h
), and initialize it to store the PID of the current process. For this lab,ugetpid()
has been provided on the userspace side and will automatically use the USYSCALL mapping. You will receive full credit for this part of the lab if theugetpid
test case passes when runningpgtbltest
.参考文章:MIT 6.S081 2021: Lab page tables
+Eliminate allocation from sbrk()
+-Your first task is to delete page allocation from the sbrk(n).
+The sbrk(n) system call grows the process’s memory size by n bytes, and then returns the start of the newly allocated region (i.e., the old size). Your new sbrk(n) should just increment the process’s size (myproc()->sz) by n and return the old size. It should not allocate memory – so you should delete the call to growproc() (but you still need to increase the process’s size!).
感想
乌龙
这里好像是因为实验改版了,我下的是2020年的实验包,在memlayout压根找不到USYSCALL和struct usyscall这俩东西。最后翻了下网上的总算找到了。
-我一开始没找到,还以为USYSCALL以及usyscall这两个都得自己写在memlayout里面,想了很久都没想出来USYSCALL的值应该设置为多少。我认为只需满足两个条件即可:1.所处内存段应该是free memory那段,也即自kernel结束(PHYSTOP)到MAXVA这一大块。2.得确保能被用户和内核都能访问到。
-前者意为虚拟地址在MAXVA和PHYSTOP之间,后者意为那段内存应该标记为PTE_U。这个范围是很宽泛的,我实在不知道要分配这期间的哪块内存,感觉也不大可能是真的自由度那么大。所以我就偷偷看了hints【悲】,想看它对这个USYSCALL应该写什么值有没有建议。结果发现这东西是实验给我们定的。遂去网上找到了它给的真正的USYSCALL值。
-+
struct usyscall{
int pid;
};-
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
int sz = myproc()->sz;
addr = sz;
myproc()->sz = sz + n;
//if(growproc(n) < 0)
// return -1;
return addr;
}用户的ugetpid只找到了一个截图:
--
恕我愚钝实在不知道该把这段代码放在哪orz于是接下来写的东西就没有自测。
-panic:freewalk leaf
一开始写好代码准备启动xv6的时候爆出了这么一个panic,搜了一下得到如下解答:
--来源:MIT-6.S081-2020实验(xv6-riscv64)十:mmap
-这时运行会发现freewalk函数panic:
+freewalk: leaf
,这是因为freewalk希望所有虚拟地址已经被解绑并释放对应的物理空间了,该函数只负责释放页表。Lazy allocation
+-Modify the code in trap.c to respond to a page fault from user space by mapping a newly-allocated page of physical memory at the faulting address, and then returning back to user space to let the process continue executing.
+You should add your code just before the
printf
call that produced the “usertrap(): …” message. Modify whatever other xv6 kernel code you need to in order to getecho hi
to work.让我得知freewalk在vm.c下面【吐槽,我一开始还以为是自由自在地走(,看到这个才反应过来是free walk,跟页表有关的】。结合freewalk的代码
--
可以知道,造成这个panic的原因是需要手动释放页表项。而在这里
-+
// in proc.c freeproc()
if(p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0;感想
思路
首先,要知道缺页中断的scause为13或15.【论我怎么知道的:被以前的实验逼出来的hhh】然后,要写在if条件的第二个分支。在该分支内,我们需要先获取出问题的地方的虚拟地址的值,然后申请新的一页,再map到当前页表中。
+一个难以察觉的错误
描述
思路是很简单的,就是有小细节需要格外注意。
+trap.c在
+mappages
时,一定不能直接传入va,必须传入PGROUNDDOWN(va)
。如果直接传入va,会爆出如下错误:+
但是,查看
+mappages
的代码:-
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
uint64 a, last;
pte_t *pte;
a = PGROUNDDOWN(va);
last = PGROUNDDOWN(va + size - 1);
for(;;){
if((pte = walk(pagetable, a, 1)) == 0)
return -1;
if(*pte & PTE_V)
panic("remap");
*pte = PA2PTE(pa) | perm | PTE_V;
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}仅仅是释放掉了对应的物理页,页表项并没有被释放。
-对比了一下别人写的,才发现原来这里也需要修改:
-+
// Free a process's page table, and free the
// physical memory it refers to.
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
//添加此句
uvmunmap(pagetable, USYSCALL, 1, 0);
uvmfree(pagetable, sz);
}我们可以看到,它在里面已经对va进行了处理了,使它变成了page-align的a变量。那么为什么,我们还要在外面再对va处理一次呢?
+其实问题不是出在
+mappages
中的a变量上,而是出现在mappages
中的last变量上。比如,令va=PGSIZE+200,则a=PGSIZE,last=2*PGSIZE。这样一来,在下面的循环中,除了添加了刚刚申请的那页的映射以外,我们还多添加了新的一页,其物理地址为mem+PGSIZE。这十分地危险!假设你要申请的va为proc->size的最后一页,那么,经过本次缺页中断之后,你事实上申请了两页,两页的地址分别为va和va+PGSIZE。而va+PGSIZE大于proc->size。也就是说,地址溢出了!
+这会导致页表释放的时候出问题。以下是页表释放的路径。
+-
// in proc.c
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, sz);
}
// in vm.c
void
uvmfree(pagetable_t pagetable, uint64 sz)
{
if(sz > 0)
uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 1);
freewalk(pagetable);
}
// in vm.c
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
if((va % PGSIZE) != 0)
panic("uvmunmap: not aligned");
for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
if((pte = walk(pagetable, a, 0)) == 0){
printf("uvmunmap walk: %p\n",a);
continue;
}
if((*pte & PTE_V) == 0){
printf("uvmunmap: %p\n",a);
continue;
}
printf("uvmunmap OK: %p\n",a);
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
*pte = 0;
}
}这样一来,问题就解决了。
-总结
因而,可以看到,如果进程想使用页的话,需要经历以下四步:
--
-- 通过kalloc获取物理页地址(可以通过该地址对页进行读写),并且记录在进程proc结构中(否则之后就获取不了了)
-- 建立mappages映射
-- 释放物理页
-- 释放PTE映射
-可见12和34都是分别一一对应的。
-代码
+
// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, or a memory allocation fails, return 0.
static struct proc*
allocproc(void)
{
struct proc *p;
//有线程池那味了
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == UNUSED) {
goto found;
} else {
release(&p->lock);
}
}
return 0;
found:
p->pid = allocpid();
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);
return 0;
}
// Allocate a usyscall page.
if((p->usyscall = (struct usyscall *)kalloc()) == 0){
release(&p->lock);
return 0;
}
//在USYSCALL写入usyscall结构体
p->usyscall->pid = p->pid;
// An empty user page table.
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// Set up new context to start executing at forkret,
// which returns to user space.
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
return p;
}
// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
if(p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;
}
// Create a user page table for a given process,
// with no user memory, but with trampoline pages.
pagetable_t
proc_pagetable(struct proc *p)
{
pagetable_t pagetable;
// An empty page table.
pagetable = uvmcreate();
if(pagetable == 0)
return 0;
// map the trampoline code (for system call return)
// at the highest user virtual address.
// only the supervisor uses it, on the way
// to/from user space, so not PTE_U.
if(mappages(pagetable, TRAMPOLINE, PGSIZE,
(uint64)trampoline, PTE_R | PTE_X) < 0){
uvmfree(pagetable, 0);
return 0;
}
// map the trapframe just below TRAMPOLINE, for trampoline.S.
if(mappages(pagetable, TRAPFRAME, PGSIZE,
(uint64)(p->trapframe), PTE_R | PTE_W) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
// 映射USYSCALL
if(mappages(pagetable, USYSCALL, PGSIZE,
(uint64)(p->usyscall), PTE_R|PTE_U) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
return pagetable;
}
// Free a process's page table, and free the
// physical memory it refers to.
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmunmap(pagetable, USYSCALL, 1, 0);
uvmfree(pagetable, sz);
}freewalk要求在uvmunmap中已经释放完所有的叶子结点。而由于uvmunmap中释放结点的va是从0递增到proc->size的,也因而,前面的那个大于proc->size的那页虽然还在页表中存在,但是不会被uvmunmap释放!这也就导致,接下来调用freewalk的时候,会发现该页的叶子结点仍然存在,从而导致
+freewalk: leaf
。可以结合uvmunmap和trap.c中的调试语句看下图的过程,可以看到非常清晰明了,0x14000这一页并没有在uvmunmap中释放!
+trap.c中的调试语句:
+-
printf("trap: %p,+PGSIZE = %p\n",PGROUNDDOWN(va),PGROUNDDOWN(va)+PGSIZE);
if(mappages(p->pagetable,va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
kfree(mem);
}问答题
--Which other xv6 system call(s) could be made faster using this shared page? Explain how.
-我觉得如果能在fork的父子进程用shared page共享页表应该会节省很多时间和空间,用个读时写。其他的倒是想不到了。不过这题会不会问的是那些在内核态和用户态穿梭频繁的system call呢?这个的话我就想不出来了。
-Print a page table
--write a function that prints the contents of a page table.
-Define a function called
-vmprint()
.It should take a
-pagetable_t
argument, and print that pagetable in the format described below.Insert
-if(p->pid==1) vmprint(p->pagetable)
in exec.c just before thereturn argc
, to print the first process’s page table.-
The first line displays the argument to
-vmprint
. After that there is a line for each PTE, including PTEs that refer to page-table pages deeper in the tree. Each PTE line is indented by a number of" .."
that indicates its depth in the tree.Each PTE line shows the PTE index in its page-table page, the pte bits, and the physical address extracted from the PTE. Don’t print PTEs that are not valid.
-In the above example, the top-level page-table page has mappings for entries 0 and 255. The next level down for entry 0 has only index 0 mapped, and the bottom-level for that index 0 has entries 0, 1, and 2 mapped.
-感想
-
很可惜,我在上面检索
-freewalk leaf
到底是什么东西的时候,不小心看到了这题需要去参照freewalk这个提示【悲】其实我觉得这点还是需要绕点弯才能想到的,可能直接想到有点难【谁知道呢,世界线已经变动了】。它这个打印页表其实最主要是考查如何遍历页表,这让人想起了walk这样的东西。但是walk是根据虚拟地址一级级找PTE的,中间很多地方会被跳过。有没有一个过程会在做事的时候遍历整个页表呢?答案是,这个过程就是释放页表的过程。释放页表才会一个个地看是否需要释放。释放页表的函数是freewalk,因而这道题参考freewalk的代码即可。
-我觉得从“遍历页表”联想到“释放页表”这点是很巧的。不过也不会很突兀,毕竟学数据结构时就知道释放就需要遍历,逆向思维有点难但问题不大。
-其他的就都挺简单的,不多赘述。
-代码
记得在defs.h中添加声明
-+
//在vm.c下
void
vmprint_helper(pagetable_t pagetable,int level)
{
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if(pte & PTE_V){
for(int j=0;j<level;j++){
printf(" ..");
}
printf("%d: pte %p pa %p\n",i,(uint64)pte,(uint64)(PTE2PA(pte)));
if((pte & (PTE_R|PTE_W|PTE_X)) == 0){
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
vmprint_helper((pagetable_t)child,level+1);
}
}
}
}
// 打印页表
void
vmprint(pagetable_t pagetable)
{
// typedef uint64 *pagetable_t;所以pagetable可以以%p形式打印
printf("page table %p\n",(uint64)pagetable);
vmprint_helper(pagetable,1);
}图:
++
debug过程
看到
+freewalk: leaf
这一错误,很容易联想到跟页表的释放有关。并且加上PGGROUNDDOWN就没问题,不加上才有问题,也很容易联想到跟mappages中多申请的那一页有关。但是具体是什么关系,这一点想要想到对我来说还是非常曲折的。我一开始,以为是因为多申请的那一页(下面简称为B页好了)很有可能是其他进程在使用的,然后其他进程在echo进程释放页表前释放了页表,从而导致B页已经free了,这样一来
+uvmunmap
说不定就能监测到对应物理页已经free,然后爆出panic。我一开始认为uvmunmap
的这句话是用来监测物理页是否free的:-
if((*pte & PTE_V) == 0){
continue;
//panic("uvmunmap: not mapped");
}问答题
--Explain the output of
-vmprint
in terms of Fig 3-4 from the text.What does page 0 contain?
-What is in page 2? When running in user mode, could the process read/write the memory mapped by page 1?
-What does the third to last page contain?
-从上面操作系统的启动来看,进程1应该是在main.c中的userinit()中创建的进程,也是shell的父进程。【确实,经实践可得shell的pid为2】
-可以来看一下userint的代码:
-+
void
userinit(void)
{
struct proc *p;
p = allocproc();
initproc = p;
// 申请一页,将initcode的指令和数据放进去
// allocate one user page and copy initcode's instructions
// and data into it.
/*
uvminit的注释:
// Load the user initcode into address 0 of pagetable,
// for the very first process.
// sz must be less than a page.
*/
uvminit(p->pagetable, initcode, sizeof(initcode));
p->sz = PGSIZE;
//为内核态到用户态的转变做准备
// prepare for the very first "return" from kernel to user.
/*
Trap Frame是指中断、自陷、异常进入内核后,在堆栈上形成的一种数据结构
*/
p->trapframe->epc = 0; // user program counter
p->trapframe->sp = PGSIZE; // user stack pointer
// 修改进程名
safestrcpy(p->name, "initcode", sizeof(p->name));
p->cwd = namei("/");
//这个也许是为了能被优先调度
p->state = RUNNABLE;
release(&p->lock);
}然后顺理成章地,这边条件==0成立,然后continue,然后直到freewalk才发现该pte未释放。
+但是,我仔细过脑子想了想,发现,就算物理页已经free了,但是,*pte依然存在,PTE_V也依然为1,这个条件是不成立的。也就是说,B页不会continue,而是会继续下面的正常释放的流程。也就是说,B页是可以正常释放的,我们的“B页已经free导致uvmunmap释放失败”的推论是错误的。
+但究竟是为什么呢?肯定跟B页有关系,但是又不是这种关系,这让我十分地苦恼且烦躁,于是我就去打了会儿游戏。边玩的时候突然注意到一件非常可疑的事情。
++
这是发生错误时退出的截图。有一个点引起了我的注意,就是echo hi并没有打印hi在console上。也就是说,这个panic是在echo执行前产生的!那么这个执行前是在哪呢?答案就是在exec中!
+-
// in exec.c
// Commit to the user image.
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
p->trapframe->epc = elf.entry; // initial program counter = main
p->trapframe->sp = sp; // initial stack pointer
// 这里!!!!!
proc_freepagetable(oldpagetable, oldsz);可见,page0是initcode的代码和数据,page1和page2用作了进程的栈,其中page1应该是guard page,page2是stack。
-不过这里从exec的角度解释其实更通用:
-+
int
exec(char *path, char **argv)
{
//分配新页表
if((pagetable = proc_pagetable(p)) == 0)
goto bad;
//elfhd应该指的是可执行文件头
// Load program into memory.
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
//...
//总之顺利读到了
uint64 sz1;
//读到了就给它分配新空间并且填入页表
if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0)
goto bad;
sz = sz1;
}
//读完文件,开始造一个新的用户栈【fork之后用户栈是不会清空的】
sz = PGROUNDUP(sz);
uint64 sz1;
if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE, PTE_W)) == 0)
goto bad;
sz = sz1;
// mark a PTE invalid for user access.造guard page
uvmclear(pagetable, sz-2*PGSIZE);
// sp为栈顶
sp = sz;
// 应该指的是栈尾
stackbase = sp - PGSIZE;
//...
}可以看到,这里调用了
+proc_freetable
,从而跟freewalk有了联系。但是,如果我们还坚持是因为B页错的,就需要找到一个可能会产生B页的地方,也就是验证shell准备执行echo命令,fork出一个子进程之后,又在exec free页表前,已经调用过sbrk函数,并且已经触发过缺页中断。这个验证其实很简单,只需要找sbrk在哪被调用过,哪边使用过heap内存【也即哪边涉及了指针赋值】就行了。
+通过全局搜索,可知sbrk在
+user/umalloc.c
下的malloc()
被使用过,而在user/sh.c
中,fork子进程之后:-
if(fork1() == 0)
runcmd(parsecmd(buf));page0就填程序。这里重点说明一下为什么page1和page2分别是guard page和stack。
-按照它的那个算术关系,stack和guard page的虚拟内存位置关系应该是这样的:
--
那为什么最后在页表中,变成了page1是gurad page,page2是stack这样上下颠倒了呢?看vm.c中的uvmalloc就能明白。
--
在253行设置了新映射。可以看到,这里设置映射的顺序是sz->sz+PGSIZE,也即先设置guard page的映射,再设置stack的映射。所以,这两位才会上下颠倒了。
-Detecting which pages have been accessed
--Some garbage collectors (a form of automatic memory management) can benefit from information about which pages have been accessed (read or write). In this part of the lab, you will add a new feature to xv6 that detects and reports this information to userspace by inspecting the access bits in the RISC-V page table. The RISC-V hardware page walker marks these bits in the PTE whenever it resolves a TLB miss.
---Your job is to implement
-pgaccess()
, a system call that reports which pages have been accessed.The system call takes three arguments. First, it takes the starting virtual address of the first user page to check. Second, it takes the number of pages to check. Finally, it takes a user address to a buffer to store the results into a bitmask (a datastructure that uses one bit per page and where the first page corresponds to the least significant bit).
-You will receive full credit for this part of the lab if the
-pgaccess
test case passes when runningpgtbltest
.感想
实验内容:
-实现
-void pgaccess(uint64 sva,int pgnum,int* bitmask);
,一个系统调用。在这里面,我们要做的是,访问从sva
到sva+pgnum*PGSIZE
这一范围内的虚拟地址对应的PTE,然后查看PTE的标记项是否有PTE_A
。有的话则在bitmask对应位标记为1.应该注意的点:
-1.需要进行内核态到用户态的参数传递 2.需要进行系统调用的必要步骤 3.PTE_A需要自己定义
-以上是初见。做完了发现,确实就是那么简单,我主要时间花费在下的实验版本不对,折腾来折腾去了可能有一个小时,最后还是选择了直接把测试函数搬过来手工调用。已经换到正确的年份版本了【泪目】
-有一点我忽视了,看了提示才知道:
--Be sure to clear
+PTE_A
after checking if it is set. Otherwise, it won’t be possible to determine if the page was accessed since the last timepgaccess()
was called (i.e., the bit will be set forever).这其中的
+parsecmd()
中malloc被使用过,并且发生了指针赋值!!也就是说,“是因为B页错的”这个结论是对的。虽然这一段debug没有改变我们要证明“是B页在释放内存中出错的”的这个目的,但是确实带给了我很多这种执行时申请内存的知识,并且也让我突然想起了可以用printf debug。于是,我就去做了上面那个在trap.c中和uvmunmap中printf的调试语句,最终成功发现了结论。
+实在是太艰苦了()这告诉我们以后千万千万要注意,是否需要用到PGGROUNDDOWN。
+一个漏掉未考虑的细节
摘自https://blog.csdn.net/m0_53157173/article/details/131349366
++
+
不过我以前好像是有考虑到这个的,但是我是这么做的:
++
也就是相当于把它在addr parse的那段代码移进了walkaddr中。但是这样是不行的,查找可知argaddr的应用范围可比walkaddr广得多……
+而为什么我下面的COW也是修改了walkaddr,而非修改argaddr,就可以达到同样的效果呢?这是因为cow只需对在内核中写用户页这种情况进行特殊处理,而这只有一个情况,也即只在copyout中发生。因而,我们只需修改walkaddr,就可以完全防范该情况了。
+Lazytests and Usertests
+-We’ve supplied you with
lazytests
, an xv6 user program that tests some specific situations that may stress your lazy memory allocator. Modify your kernel code so that all of bothlazytests
andusertests
pass.也就是说每次检查到一个,就需要手动清除掉PTE_A标记。
-还有一点以前一直没注意到的,头文件的引用需要注意次序。比如说要是把
-spinlock.h
放在proc.h
后面,就会寄得很彻底。代码
那些系统调用的登记步骤就先省略了。
-+
// kernel/sysproc.c
uint64
sys_pgaccess(void)
{
uint64 sva;
int pgnum;
uint64 bitmask;
if(argaddr(0,&sva) < 0 || argint(1, &pgnum) < 0 || argaddr(2, &bitmask) < 0)
return -1;
return pgaccess((void*)sva,pgnum,(void*)bitmask);
}感想
一个绷不住的错误
其实很简单,按照提示一步步做就行了。为什么我做得那么久那么崩溃呢?知道原因后我都笑嘻了。
+在第一步修改
+sys_sbrk()
的时候,我一下子没多想,使用了一句int sz = myproc()->sz
,其实本来应该使用uint64的,使用int会溢出。这个伏笔就一直隐含到这里,然后大坑了我一笔。一开始是发现
+lazytests
的第二个,也就是oom过不去。我想了很久,也去网上找了别人的代码一步步对比下来看了,没有发现特别大的问题。于是我就在walk和sys_sbrk分别留下了调试信息:-
// in walk()
if(va >= MAXVA){
printf("walk:va=%p,p->sz=%p,MAXVA=%p,pgva=%p\n",va,myproc()->sz,MAXVA,PGROUNDDOWN(va));
panic("walk");
}+
// kernel/pgaccess.c
int
pgaccess(void* sva,int pgnum,void* bitmask){
if(pgnum > 32){
printf("pgaccess: range too big.\n");
exit(1);
}
int kmask = 0;
struct proc* p = myproc();
for(int i=0;i<pgnum;i++){
pte_t* pte = walk(p->pagetable,(uint64)sva+i*PGSIZE,0);
// 映射不存在,或者没有被访问过
if(!pte || !(*pte & PTE_A)){
continue;
}
kmask = (kmask | (1<<i));
*pte = (*pte & (~PTE_A));
}
copyout(p->pagetable,(uint64)bitmask,(char*)(&kmask),sizeof(int));
return 1;
}-
// in sys_sbrk()
if(n >= 0){
uint64 tmp = n + sz;
if(tmp > MAXVA || n + sz < n) return -1;
myproc()->sz = tmp;
//printf("haha sb!sz=%p,n=%p\n",myproc()->sz,n);
}A kernel page table per process
-The goal of this section and the next is to allow the kernel to directly dereference user pointers.
+然后发现了这样的输出:
++
可以看到,最后一次sz发生了数值溢出。
+但是,此时我并没有悔改。我反而认为,“原本代码就是这么写的”。也就是说,我认为
+int sz
是它原本内核代码给的。。。。。。在这样的情况下,我选择加上这样的条件判断:+ +
if(tmp > MAXVA || ((tmp >> 31)& 1) == 1) return -1;之后确实没有溢出了,但是test fail了。此时我想,为什么非要用int而不用uint64呢?一阵令人不寒而栗的预感袭来,我连忙去看了
+proc.h
里的sz的定义,发现,sz原本就应该是uint64类型的,是我错辣【悲】只能说起到一种很好的教训。主要是这种问题实在没有想过自己会犯
+代码
+++
- Handle the parent-to-child memory copy in fork() correctly.
++
// in vm.c uvmcopy()
if((pte = walk(old, i, 0)) == 0)
continue;
//panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
continue;
//panic("uvmcopy: page not present");--Your first job is to modify the kernel so that every process uses its own copy of the kernel page table when executing in the kernel.
-Modify
+struct proc
to maintain a kernel page table for each process, and modify the scheduler to switch kernel page tables when switching processes. For this step, each per-process kernel page table should be identical to the existing global kernel page table. You pass this part of the lab ifusertests
runs correctly.+
- Handle negative sbrk() arguments.
+感想
这个其实平心而论不难,思路很简单。写着不难是不难,但想明白花费了我很多时间。
-它这个要求我们修改kernel,使得每个进程都有一份自己的kernel page。至于要改什么,围绕着proc.c中,参照pagetable的生命周期摁改就行。还有一个地方它也提示了,就是要在swtch之前更换一下satp的值。
-接下来,我说说我思考的几个点以及犯错的地方。
-为什么要这么干
看完题目,我的第一印象是,这么干有啥用。。。因为我觉得以前那个所有进程共用内核页表确实很好了,没有必要每个进程配一个后来才发现,这个跟下面那个是连在一起的,目的是
-allow the kernel to directly dereference user pointers.
。所以,我们下面会把用户的pgtbl和这里dump出来的kpgtbl合在一起。具体来说:
-通常,进行地址翻译的时候,计算机硬件(即内存管理单元MMU)都会自动的查找对应的映射进行翻译(需要设置
-satp
寄存器,将需要使用的页表的地址交给该寄存器)。然而,在xv6内核需要翻译用户的虚拟地址时,因为内核页表不含对应的映射,计算机硬件不能自动帮助完成这件事。因此,我们需要先找到用户程序的页表,仿照硬件翻译的流程,一步一步的找到对应的物理地址,再对其进行访问。【
-walkaddr
】这也就会导致copyin之类需要涉及内核和用户态交互的函数效率低下。为了解决这个问题,我们尝试将用户页表也囊括进内核页表映射来。但是,如果将所有进程的用户页表都合并到同一个内核全局页表是不现实的。因而,我们决定换一个角度,让每个进程都仅有一张内核态和用户态共用的页表,每次切换进程时切换页表,这样就构造出了个全局的假象。
-这两次实验就是为了实现该任务。在本次实验中,我们首先先实现内核页表的分离。
-关于myproc()
在allocproc中初始化的时候,我一开始是这么写的:
-- -
// in proc.c allocproc()
perproc_kvminit();+
// in vm.c
pagetable_t
perproc_kvminit()
{
struct proc* p = myproc();
p->kpgtbl = (pagetable_t) kalloc();
memset(p->kpgtbl, 0, PGSIZE);
// uart registers
pkvmmap(p->kpgtbl,UART0, UART0, PGSIZE, PTE_R | PTE_W);
// ...
return pt;
}-
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
uint64 sz = myproc()->sz;
addr = sz;
if(n >= 0){
uint64 tmp = n + sz;
if(tmp > MAXVA || n + sz < n) return -1;
myproc()->sz = tmp;
//printf("haha sb!sz=%p,n=%p\n",myproc()->sz,n);
} else{
if(n + sz > 0)
myproc()->sz = uvmdealloc(myproc()->pagetable, sz, sz + n);
else
return -1;
}
//if(growproc(n) < 0)
// return -1;
return addr;
}这样会死得很惨,爆出如下panic:
--
通过hints的调试贴士
--A missing page table mapping will likely cause the kernel to encounter a page fault. It will print an error that includes
+sepc=0x00000000XXXXXXXX
. You can find out where the fault occurred by searching forXXXXXXXX
inkernel/kernel.asm
.+
- +
Kill a process if it page-faults on a virtual memory address higher than any allocated with sbrk().
+- +
Handle out-of-memory correctly: if kalloc() fails in the page fault handler, kill the current process.
+- +
Handle faults on the invalid page below the user stack.
+我发现程序在这里绷掉了:
-+
p->kpgtbl = (pagetable_t) kalloc();-
else if(scause == 13 || scause == 15){
// 缺页中断
uint64 va = r_stval();
if(va >= p->sz || va < PGROUNDUP(p->trapframe->sp) ||PGROUNDDOWN(va) >= MAXVA){
//printf("va=%p stack=%p\n",va,PGROUNDUP(r_sp()));
p->killed = 1;
} else{
char* mem = kalloc();
if(mem != 0){
memset(mem, 0, PGSIZE);
//printf("trap: %d %p\n",p->pid,PGROUNDDOWN(va));
if(mappages(p->pagetable,PGROUNDDOWN(va), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
kfree(mem);
p->killed = 1;
}
} else {
p->killed = 1;
}
}
}而且显而易见,是系统启动时崩的。
-经过了漫长的思考,我震惊地发现了它为什么崩了()
-首先,这段代码语法上是没有问题的。它固然犯了发布未初始化完成的对象这样的并发错误【我有罪】,也破坏了proc的封装性【proc中的很多私有属性本来应该作用域仅在proc.c中的。此处为了能让vm.c访问到proc中的属性,不得不给vm.c添上了proc.h的头文件】,但是它并不是语法错误,还是能用的。我做了这样的测试样例证明它没有问题:
-+
typedef int pagetable_t;
struct proc{
pagetable_t kpgtbl;
};
struct proc processes[MAX];
struct proc* myproc(){
return &processes[0];
};
void kvminit(){
myproc()->kpgtbl = 1;
}
int main(){
struct proc* p = &processes[0];
kvminit();
printf("%d",p->kpgtbl);
return 0;
}+++
+- Handle the case in which a process passes a valid address from sbrk() to a system call such as read or write, but the memory for that address has not yet been allocated.
+我认为这里要是引起一个缺页中断可能会更酷,可能可以像lazytests里面这么做:
+-
char *i, *prev_end, *new_end;
prev_end = sbrk(REGION_SZ);
new_end = prev_end + REGION_SZ;
// 这里触发了多次缺页中断
for (i = prev_end + PGSIZE; i < new_end; i += PGSIZE * PGSIZE)
*(char **)i = i;我一路顺着os启动的路径找,也想不出来这能有什么错,因而非常迷茫。
-此时我灵光一闪,会不会是myproc()在os刚启动的时候是发挥不了作用的?于是我一路顺着myproc的代码看下去:
-+
struct proc*
myproc(void) {
push_off();
struct cpu *c = mycpu();
struct proc *p = c->proc;
pop_off();
return p;
}之后有机会再试试233
+【试了一下,发现是可以的。在COW fork的
+感想—一些错误和思考—在内核态中引发并处理缺页中断
这部分内容中详细说明了具体要怎么做。】Lab: Copy-on-Write Fork for xv6
++Parent and child can safely share phyical memory using copy-on-write fork, driven by page faults.
+RISC-V has three different kinds of page fault: load page faults (when a load instruction cannot translate its virtual address), store page faults (when a store instruction cannot translate its virtual address), and instruction page faults (when the address for an instruction doesn’t translate).
+The basic plan in COW fork is for the parent and child to initially share all physical pages, but to map them read-only. Thus, when the child or parent executes a store instruction, the RISC-V CPU raises a page-fault exception. In response to this exception, the kernel makes a copy of the page that contains the faulted address. It maps one copy read/write in the child’s address space and the other copy read/write in the parent’s address space. After updating the page tables, the kernel resumes the faulting process at the instruction that caused the fault.
+PS【这个很重要】: COW fork() makes freeing of the physical pages that implement user memory a little trickier. A given physical page may be referred to by multiple processes’ page tables, and should be freed only when the last reference disappears.
+感想
思路
思路还是很直观的。
+我们只需要在fork的时候,标记父子进程的所有PTE都为read-only,然后之后就会遇到不同scause的缺页中断,针对特定的scause,新建物理页面,拷贝物理页面,然后重新设置映射即可。而对于其提出的需要标记某页是否能够释放,则需要统计每页的ref数,当ref==1的时候才可以释放。
+分析
总分析
其实可以把任务简单拆分为三部分。第一部分是实现基本的cow fork的逻辑,第二部分是引用计数释放内存,第三部分是解决copyin/copyout时在内核态发生的缺页中断。我认为本实验的难点事实上在第二部分【悲】我可能有大于3/4的时间都花在第二部分上了吧。
+第一部分是实现cow fork的基本逻辑,也就是修改fork中对页表的拷贝以及在usertrap中添加对缺页中断的处理,这很直观,没什么好说的。
+第三部分要么跟上面的lazy allocation一样,在
+kernel/vm.c walkaddr()
中把缺页中断搬过去,要么向我在主要难点与错误—在内核态中引发并处理缺页中断
这一部分那样做。我们分析这一部分主要讲的是我认为最难的地方,也就是第二部分。其实第二部分的思路也很直观:创建一个数组,index为所有能用的内存的
+address/PGSIZE
,用来记录引用数;然后在每次增加引用时,对应元素++;每次减少引用时,对应元素–。虽然思路很简单很直观,但是实现起来非常地非常地非常地考验细节(我就非常不擅长这一点)。下面,我就先阐述一下第二部分的这个方法需要分割为哪几部分,其他我遇到的印象较深的bug和对一些地方的思考,都放在了下一部分,也即
+主要难点与错误
。引用数实现分析
++创建一个数组,index为所有能用的内存的
+address/PGSIZE
,用来记录引用数;然后在每次增加引用时,对应元素++;每次减少引用时,对应元素–。数组的大小和数据类型
由
+kernel/kalloc.c
中的kinit()
:-
void
kinit()
{
initlock(&kmem.lock, "kmem");
// freerange用来把参数地址范围内的物理页加入freelist中
// end是内核的结束地址
freerange(end, (void*)PHYSTOP);
}那么,mycpu()获得的cpu的proc是怎么得到的呢?
-我搜寻了一下os启动代码,发现了cpu的proc得到的路径。
-+
void
main()
{
if(cpuid() == 0){
consoleinit();
printfinit();
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
//...很多很多init
userinit(); // first user process
__sync_synchronize();
started = 1;
} else {
// ...
}
//调度执行第一个进程
scheduler();
}可知,事实上,我们整个程序,包括用户和内核,能用的内存空间为0~PHYSTOP。因而,我们事实上只需要建一个
+PHYSTOP/PGSIZE
这么大的数组就行。我算了一下大概是2^19次方。然后,我感觉这种小系统应该不会有过多的对某一页的重复引用,因而,为了节省空间,我将数据类型定为了char。最好还是别定成uchar,因为这东西要是0–的话会溢出变为255,很可怕。
+什么时候增减引用
++我认为这里是非常考验细节和头脑清晰度的,也就是我卡了很久最后也没弄出来的部分【悲】
+可以分为三种情况来讲。我们的引用计数必须完美适应这三种情况:
++
+- +
不经由页表,通过kalloc和kfree直接使用物理页
+这就要求我们在kalloc的时候置引用数为1,然后kfree的时候对引用数先-1,再判断是否归零。
+- +
经由页表,但与cow fork无关
+增加页表项:mappages->kalloc,因而满足要求1即可。
+删除页表项:uvmunmap。当do_free==1时,满足要求1即可。
+- +
经由页表,与cow fork有关
+copy父进程页表时:在cowcopy中,每增加一次子进程的映射,就需要增加一次引用数
+在用户态/内核态发生缺页中断:发生缺页中断后,对原来物理页的引用数需要-1【我就是漏了这一点……】
+删除页表项:uvmunmap。当do_free==0时,当对应页表项有COW标记,则减少引用数
+所以,我们需要在三个文件进行修改:
++
+- +
kalloc.c
+增加数组定义,在kalloc和kfree中增加引用数修改
+- +
vm.c
+在cowcopy和uvmunmap中增加引用数修改
+- +
trap.c
+在usertrap的缺页中断中增加引用计数修改
+并发安全
++这里我也没想到【悲】
+由于我们的pages数组会在多个文件、多个进程间使用,所以它必须在被锁保护的区域中被使用。
+主要难点与错误
scause=2
+
这个发生在我还没有实现第二部分的时候。搜索了一下,scause=2为
+Illegal instruction
,而且sepc的这个1004的值也非常诡异。这应该是因为fork子进程释放了指令段内存,导致主进程执行错误kernel无法启动
在kinit中
+-
void
kinit()
{
initlock(&kmem.lock, "kmem");
initlock(&pages_lock,"pages");
memset(pages, 0, (2<<19));
freerange(end, (void*)PHYSTOP);
}创建完进程后,就进入scheduler进行进程的调度:
-+
void
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();
// ...
int found = 0;
for(p = proc; p < &proc[NPROC]; p++) {
// ...
//在这里!!!!
c->proc = p;
swtch(&c->context, &p->context);
c->proc = 0;
// ...会通过freerange初始化freelist。在freerange中:
+-
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
kfree(p);
}
}因而,c->proc是在创建进程的第一次调度后初始化的,也即,myproc只有在执行第一次scheduler之后才可以调用。而!!!
-当执行调度前的userinit时:
-+
void
userinit(void)
{
struct proc *p;
p = allocproc();
initproc = p;会对每一项进行一次kfree。因而,我们需要在kfree前先增加一次引用,要不然会寄。
+在缺页中断时减少对物理页的引用数
+
注意此处不能直接让pages[pa/PGSIZE]–,一定要借助kfree。当此进程为引用pa的最后一个进程的时候,如果仅减少引用数,就会造成内存泄漏。kfree可以既减少引用数,又在适当的时候对物理页释放,可谓一举两得。kfree的这个双重作用思想也在uvmunmap中体现了。
+在内核态中引发并处理缺页中断
++Modify copyout() to use the same scheme as page faults when it encounters a COW page.
+我们所做的第一第二部分仅仅是完成了对来自用户态的缺页中断的完美处理,还尚未处理来自内核态的缺页中断。因而,这个修改copyin和copyout的点实际上就是要我们处理内核态的缺页中断。
+这次实验跟上次的lazy allocation一样,都可以直接在walkaddr进行特殊处理,并且差不多要把usertrap的全部代码挪过来【具体见lazy allocation的代码】。不过,我想出了另一个流氓的方法(也就是说其实原理感觉是不大对233)。我选择直接在kernel引发一个访问用户页面的缺页中断,然后在kerneltrap中处理这个中断,就像usertrap一样。
+但由于在
+walkaddr
中发生的中断处于内核状态下,所以就进不了usertrap。我们应该在kerneltrap中再次添加和usertrap一样的中断处理。我们会像这样引发一个中断:-
if((*pte & PTE_COW) != 0){
if((*pte & PTE_W) == 0){
*(char*)va = 1;// 此处不能用pa哦它进行了allocproc。我们亲爱的allocproc接下来就会调用perproc_kvminit,然后perproc_kvminit中调用myproc。此时尚未进行初次调度,因而c->proc未初始化,myproc返回的是0,也即null。这样一来,
-myproc()->kpgtbl
就发生了空指针异常,也即scause = 15——写入页错误。因而,对于myproc()的调用需要慎之又慎。
-系统调用
系统调用时,是如何知道要用的是p中的内核页表而非global内核页表呢?
-依然还是从os的启动说起。
-在main.c中,kvminithart开启了页表,此时的页表为全局的内核页表:
-+
// Switch h/w page table register to the kernel's page table,
// and enable paging.
void
kvminithart()
{
w_satp(MAKE_SATP(kernel_pagetable));
sfence_vma();
}然后在kerneltrap中这样处理:
+-
if(r_scause() == 15){
// 只要写入引起的缺页中断
uint64 va = r_stval();
pte_t *pte;
uint64 pa;
uint flags;
if((pte = walk(p->pagetable, va, 0)) == 0)
p->killed = 1;
else if((*pte & PTE_V) == 0)
p->killed = 1;
else {
sepc += 4;
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
char* mem;
if((mem = kalloc())!=0){
memmove(mem, (char*)pa, PGSIZE);
// 设置为新的物理页地址
*pte = PA2PTE(mem);
kfree((void*)pa);
// 设置新的flag,标记为可写
flags = (flags | PTE_W | PTE_COW);
*pte = ((*pte) | flags);
} else{
p->killed = 1;
}
}
} else if((which_dev = devintr()) == 0){当userinit被调度时,全局的内核页表被换成了proc中的内核页表:
-+
// in proc.c scheduler()
p->state = RUNNING;
w_satp(MAKE_SATP(p->kpgtbl));
sfence_vma();
c->proc = p;
swtch(&c->context, &p->context);但是,这样做是不行的。
+++会在这里卡住,会无限次不断进入kerneltrap。
++
造成这个的原因,经过一番曲折的debug之后,我发现,只要像usertrap中的syscall分支一样:
+-
if(r_scause() == 8){
// system call
// 重点是这里
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;
// ...
}但是这样还没有结束。因为我们除了得更换目前的页表,还得更换trapframe中的内核页表相关的东西:
-+
struct trapframe {
/* 0 */ uint64 kernel_satp; // kernel page table
/* 8 */ uint64 kernel_sp; // top of process's kernel stack
}加上这句话就行:
+-
sepc += 4;为啥还要更换trapframe中的呢?因为以后系统调用的时候,uservec是从这里读取值来作为内核栈和内核页表的来源的:
-+
# in uservec
# restore kernel stack pointer from p->trapframe->kernel_sp
# 完成了内核栈的切换
ld sp, 8(a0)
# 完成了页表的切换
# restore kernel page table from p->trapframe->kernel_satp
ld t1, 0(a0)
csrw satp, t1
sfence.vma zero, zero所以,结果就非常显而易见了,是因为一直卡在这句话执行不下去:
+-
*(char*)va = 1;所以,为了以后系统调用能顺利自发进行,我们需要把栈帧也一起换掉。怎么换呢?我们是否还要在一些地方人工把trapframe的值设置为我们自己的内核栈内核页表?答案是,不用!这些会由其他代码自动完成。
-前面说到userinit的进程p被调度,satp换成了我们自己的内核页表。那么,在之后的内核态,satp都将保持我们自己的内核页表。当要返回用户态时,会执行如下代码:
-+
// in usertrapret
// 重置trapframe
p->trapframe->kernel_satp = r_satp(); // kernel page table
p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack这是因为缺页中断会返回到原代码句中执行,所以就会继续回到这句话。而我们知道,此时正处于内核态,并没有开启地址映射,所以此处其实是非法地址越界了。但是我们的目的确实已经达到了(因为va通过stval寄存器被传递到了kerneltrap),所以我们这里只需跳过这句即可。
+这也是为什么说我这个方法虽然实现了,但本质上其实非常流氓,不算是kerneltrap。
+++Update:验收的时候跟学长说了一下这个点,学长表示不算流氓,反而在内核(至少内核赛)中算是一个比较通用的手法hh没想到还误打误撞上了
+它带来的好处是,当地址不合法的时候可以减少开销。
+具体来说,内核中一般会将地址空间分为多个vma,因而检查地址越界无需像xv6那样简单查页表,只需查地址是否在对应的vma中即可。所以,直接把这东西转到一个硬件的缺页中断中实现,事实上确实是减少了地址非法时的开销。
+除了这一点外,还有一点很重要的是,由于
+walkaddr
是需要返回一个pa的,因而我们需要手动再把pa在缺页中断后更新一下:-
pa = PTE2PA(*pte);
if((*pte & PTE_COW) != 0){
if((*pte & PTE_W) == 0){
*(char*)va = 1;
pa = PTE2PA(*pte);
}
}
return pa;satp内的值为我们自己的内核页表,而非全局页表。因而这样栈帧中的页表就会被自然而然地写入为进程的内核页表。之后返回用户态,以及之后之后的各种中断,就都会一直使用自己的内核页表了。【试了一下,这里如果改成非即时从satp读,而是默认的kernel_pagetable的话,会一直死循环】
-不得不说,真是设计精妙啊!!!不过我觉得,要是这里写成kernel_pagetable,然后让我们自己改的话将是薄纱(。当然它应该也不会这么做,因为,kernel_pagetable事实上是不对外发布的。它这里这么写热读,最直接的原因还是因为读不到kernel_pagetable。这算是无心插柳柳成荫吗233
-释放页表但不释放物理内存
其实答案就在它给的
-proc_freepagetable
里。+
// Free a process's page table, and free the
// physical memory it refers to.
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, sz);
}总之,做了这两个关键步骤后,也能启动了,也能过cowtest了。所以下面的代码也就贴上了这里的版本。
+心得
本次实验耗时经典五小时(包含笔记时间就是六个半小时了hhh),算是平均水平。很遗憾也很难受的一点是,我的错误最终还是没有自己想出来,而是参考了别人的代码才改对的。思路很简单,但是细节也依然非常多非常坑,还是得再加把劲。
+代码
我们只需要在fork的时候,标记父子进程的所有PTE都为read-only,然后之后就会遇到不同scause的缺页中断,针对特定的scause,新建物理页面,拷贝物理页面,然后重新设置映射即可。而对于其提出的需要标记某页是否能够释放,则需要统计每页的ref数,当ref==1的时候才可以释放。
+定义COW标记
-
// in kernel/riscv.h
// ...-
uvmfree
遍历页表,对每个存在的页表项,都试图找到其物理内存,并且释放物理内存和表项。如果页表项存在,但页表项对应的物理内存不存在,就会抛出freewalk leaf
的异常。-
uvmunmap
会释放掉参数给的va的页表项,最后一个参数表示释放or不释放。在这里,使用这两个的组合技,就可以达到不释放
-TRAMPOLINE
和TRAPFRAME
的物理内存,又不会让uvmfree出错的效果。代码
初始化
初始化kpgtbl。由于现在内核栈存在各自的内核页表而非global内核页表中,所以在procinit中的对内核栈的初始化也得放在这:
-+
// in proc.c allocproc()
// An empty user page table.
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
p->kpgtbl = perproc_kvminit();
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int) (p - proc));
pkvmmap(p->kpgtbl,va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va;引用数组初始化
-
// in kernel/kalloc.c
char pages[(2<<19)];
struct spinlock pages_lock;
void
kinit()
{
initlock(&kmem.lock, "kmem");
initlock(&pages_lock,"pages");
memset(pages, 0, (2<<19));
freerange(end, (void*)PHYSTOP);
}
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
pages[(uint64)p/PGSIZE] = 1;
kfree(p);
}
}+
// in vm.c
pagetable_t
perproc_kvminit()
{
pagetable_t pt = (pagetable_t) kalloc();
memset(pt, 0, PGSIZE);
// uart registers
pkvmmap(pt,UART0, UART0, PGSIZE, PTE_R | PTE_W);
// virtio mmio disk interface
pkvmmap(pt,VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
// CLINT
pkvmmap(pt,CLINT, CLINT, 0x10000, PTE_R | PTE_W);
// PLIC
pkvmmap(pt,PLIC, PLIC, 0x400000, PTE_R | PTE_W);
// map kernel text executable and read-only.
pkvmmap(pt,KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
// map kernel data and the physical RAM we'll make use of.
pkvmmap(pt,(uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
// map the trampoline for trap entry/exit to
// the highest virtual address in the kernel.
pkvmmap(pt,TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
return pt;
}申请和释放页时增删引用
-
// in kernel/kalloc.c
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
if(r){
acquire(&pages_lock);
// 在这
pages[(uint64)r/PGSIZE] = 1;
release(&pages_lock);
}
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
void
kfree(void *pa)
{
struct run *r;
acquire(&pages_lock);
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP || pages[(uint64)pa/PGSIZE] <= 0 )
panic("kfree");
// 每次kfree都会减少引用
pages[(uint64)pa/PGSIZE]--;
// 说明此时页面还被其他东西引用着,不能释放
if(pages[((uint64)pa)/PGSIZE] > 0){
release(&pages_lock);
return;
}
release(&pages_lock);
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}+
// in vm.c
void
pkvmmap(pagetable_t pgtbl,uint64 va, uint64 pa, uint64 sz, int perm)
{
// 当第一个进程开始时,mycpu->proc = null,所以这里不能调用myproc
if(mappages(pgtbl, va, sz, pa, perm) != 0)
panic("kvmmap");
}修改fork时对页表的复制操作,并标记引用数增加
-
// in kernel/proc.c fork()
// Copy user memory from parent to child.
if(cowcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
// in kernel/vm.c
int
cowcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0)
panic("cowcopy: pte should exist");
if((*pte & PTE_V) == 0)
panic("cowcopy: page not present");
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
// 去除flag中的PTE_W,并且给父子的都安上没有PTE_W的flag
flags = (flags & (~PTE_W));
flags = (flags | PTE_COW);
*pte = ((*pte) & (~PTE_W));
*pte = ((*pte) | PTE_COW);
if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
goto err;
}
// 标记物理页的引用数增加
acquire(&pages_lock);
pages[pa/PGSIZE]++;
release(&pages_lock);
}
return 0;
err:
// 失败了不能释放物理内存
uvmunmap(new, 0, i / PGSIZE, 0);
return -1;
}swtch时切换页表
+
// in proc.c scheduler()
p->state = RUNNING;
w_satp(MAKE_SATP(p->kpgtbl));
sfence_vma();
c->proc = p;
swtch(&c->context, &p->context);
//...
if(found == 0) {
// 没有进程运行时使用全局kernel_pagetable
kvminithart();
intr_on();
asm volatile("wfi");
}处理缺页中断,标记引用数减少
-
} else if(r_scause() == 15){
// 只要求写入引起的缺页中断
uint64 va = r_stval();
pte_t *pte;
uint64 pa;
uint flags;
if((pte = walk(p->pagetable, va, 0)) == 0)
p->killed = 1;
else if((*pte & PTE_V) == 0)
p->killed = 1;
else {
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
char* mem;
if((mem = kalloc())!=0){
memmove(mem, (char*)pa, PGSIZE);
// 设置为新的物理页地址
*pte = PA2PTE(mem);
// 减少引用,引用归零时释放
kfree((void*)pa);
// 设置新的flag,标记为可写
flags = (flags | PTE_W | PTE_COW);
*pte = ((*pte) | flags);
} else{
p->killed = 1;
}
}
}修改kvmpa
+
uint64
kvmpa(uint64 va)
{
uint64 off = va % PGSIZE;
pte_t *pte;
uint64 pa;
pte = walk(myproc()->kpgtbl, va, 0);
if(pte == 0)
panic("kvmpa");
if((*pte & PTE_V) == 0)
panic("kvmpa");
pa = PTE2PA(*pte);
return pa+off;
}uvmunmap时减少引用数
-
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
// ...
for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
// ...
uint64 pa = PTE2PA(*pte);
if(do_free){
kfree((void*)pa);
}else {
acquire(&pages_lock);
if(((*pte) & PTE_COW) != 0){
pages[pa/PGSIZE]--;
}
release(&pages_lock);
}
*pte = 0;
}
}释放
+
// in kernel.proc.c freeproc()
if(p->kpgtbl)
proc_freekpgtbl(p->kpgtbl,p->kstack);
p->kpgtbl = 0;修改walkaddr
在walkaddr中触发缺页中断
-
uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
pte_t *pte;
uint64 pa;
if(va >= MAXVA)
return 0;
pte = walk(pagetable, va, 0);
if(pte == 0)
return 0;
if((*pte & PTE_V) == 0)
return 0;
if((*pte & PTE_U) == 0)
return 0;
pa = PTE2PA(*pte);
// 在这里
if((*pte & PTE_COW) != 0){
if((*pte & PTE_W) == 0){
// 触发缺页中断
*(char*)va = 1;
// 更新pa值
pa = PTE2PA(*pte);
}
}
return pa;
}+
extern char etext[]; // kernel.ld sets this to end of kernel code.
void
proc_freekpgtbl(pagetable_t pagetable,uint64 stack )
{
uvmunmap(pagetable, UART0, 1, 0);
uvmunmap(pagetable, VIRTIO0, 1, 0);
uvmunmap(pagetable, CLINT, 0x10000/(uint64)PGSIZE, 0);
uvmunmap(pagetable, PLIC, 0X400000/(uint64)PGSIZE, 0);
uvmunmap(pagetable, KERNBASE, (uint64)((uint64)etext-KERNBASE)/PGSIZE, 0);
uvmunmap(pagetable, (uint64)etext,(PHYSTOP-(uint64)etext)/PGSIZE, 0);
//kvmmap(KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, stack, 1,1 );
uvmfree(pagetable, 0);
}在kerneltrap内补上对缺页中断的处理
-
void
kerneltrap()
{
uint64 sepc = r_sepc();
// ...
if(r_scause() == 15){
// 只要写入引起的缺页中断
uint64 va = r_stval();
pte_t *pte;
uint64 pa;
uint flags;
if((pte = walk(p->pagetable, va, 0)) == 0)
p->killed = 1;
else if((*pte & PTE_V) == 0)
p->killed = 1;
else {
// 注意,这个很重要!!!!!
sepc += 4;
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
char* mem;
if((mem = kalloc())!=0){
memmove(mem, (char*)pa, PGSIZE);
// 设置为新的物理页地址
*pte = PA2PTE(mem);
kfree((void*)pa);
// 设置新的flag,标记为可写
flags = (flags | PTE_W | PTE_COW);
*pte = ((*pte) | flags);
} else{
p->killed = 1;
}
}
} else if((which_dev = devintr()) == 0){
printf("scause %p\n", scause);
printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
panic("kerneltrap");
}
// ...
}Simplify
copyin/copyinstr
-+]]>参考:
- -
将主存储器以及各种外设接口卡里面内置的存储器连接起来,就形成了内存地址空间。内存地址空间中的地址是真实的物理地址。RISC-V架构的指令使用的地址是虚拟地址。为了通过指令中的虚拟地址访问到真实的物理内存,需要进行从虚拟地址到物理地址的转换。从虚拟地址到物理地址的转换,就需要通过页表来实现。
+在RISC-V指令集中,当我们需要开启页表服务时,我们需要将我们预先配置好的页表首地址放入 satp
寄存器中。从此之后, 计算机硬件 将把访存的地址 均视为虚拟地址 ,都需要通过硬件查询页表,将其 翻译成为物理地址 ,然后将其作为地址发送给内存进行访存。
xv6采用的指令集标准为RISC-V标准,其中页表的标准为SV39标准,也就是虚拟地址最多为39位。
+虚实地址翻译流程:
+satp
中。satp
找到根页表的物理页帧号,转成物理地址(Offset
为0),通过虚拟地址的L2
索引,找到对应的页表项。Offset
为0),通过虚拟地址的L1
索引,找到对应的页表项。Offset
为0),通过虚拟地址的L0
索引,找到对应的页表项。Offset
,转成物理地址(Offset
和虚拟地址Offset
相同)。页表由页表项PTE(Page Table Entries)构成,每个页表项由44位的PPN(Physical Page Number)和一些参数flag组成。
+-+The kernel’s
+copyin
function reads memory pointed to by user pointers. It does this by translating them to physical addresses, which the kernel can directly dereference. It performs this translation by walking the process page-table in software. Your job in this part of the lab is to add user mappings to each process’s kernel page table (created in the previous section) that allowcopyin
(and the related string functioncopyinstr
) to directly dereference user pointers.Each PTE contains flflag bits that tell the paging hardware how the associated virtual address is allowed to be used. PTE_V indicates whether the PTE is present: if it is not set, a reference to the page causes an exception (i.e. is not allowed). PTE_R controls whether instructions are allowed to read to the page. PTE_W controls whether instructions are allowed to write to the page. PTE_X controls whether the CPU may interpret the content of the page as instructions and execute them. PTE_U controls whether instructions in user mode are allowed to access the page; if PTE_U is not set, the PTE can be used only in supervisor mode.
+这个表项的几个参数定义在kernel/riscv.h中的341行左右。
虚拟地址有64bit,其中25bits未使用,39bits包含了27位的PTE索引号以及12位的offset。
+物理地址有56位,由PPN和offset拼接组成。
+以单页表为例,物理地址形成过程如下图所示。
+每个页表项PTE索引着一页。因而,每一页的大小为2^12=4096B。单页表中PTE的索引号有2^27个,因而单页表中表项有134217728个,即可以代表134217728页。页表实际上也是以页的形式存储的。因而单页表需要的存储空间为(2^27x7)/2^12=2^15x7=229376页。
+RISC-V架构中真实情况是会有三级页表。三级页表结构相比于单级页表结构,会占据更多的物理存储空间。
+每个页表项PTE索引着一页,这一页可能代表着另一个页表,也可能代表着内存中需要的指令和数据。因而,每一页的大小为2^12=4096B。三页表中,一级页表中PTE的索引号有512个,可以代表的物理内存页数有512x515x512=2^27页,即可以代表134217728页。页表实际上也是以页的形式存储的,一个页表有2^9x7个字节,可以存储在1页中。因而三页表需要的存储空间为1+2^9+2^18 = 262657页。
+三级页表结构相比于单级页表结构,可以节省更多内存空间。
--Replace the body of
+copyin
inkernel/vm.c
with a call tocopyin_new
(defined inkernel/vmcopyin.c
); do the same forcopyinstr
andcopyinstr_new
. Add mappings for user addresses to each process’s kernel page table so thatcopyin_new
andcopyinstr_new
work.
这题很直观的思路是,在每个user pagetable添加映射的地方也添加kpgtbl的映射。但问题是,“每个user pagetable添加映射的地方”都是哪?
-我一开始想着偷偷懒,直接在proc.c和vm.c中每个操纵pagetable的地方都加上对kpgtbl的操纵。但很快我就给搞晕了。这时候,我心中萌生一计【PS:下面说的最后都没成功】:我直接快进到把proc结构中的pagetable属性给删了,然后每个出现p->pagetable的地方,都用p->kpgtbl代替,直接让两表合为一表,然后之后make的时候哪里报错改哪里,这不就一劳永逸地把所有出现pagetable的地方都改为kpgtbl了嘛。我振奋地去试了一下,将所有地方出现的pagetable都替换成了kpgtbl,把proc.c中的proc_pagetable()
和proc_freepagetable()
的出现的地方都换成了perproc_kvminit()
以及proc_freekpgtbl()
,还做了一个小细节,就是在userinit中调用的uvminit中,我把这样:
void |
换成了这样:
-void |
最后,在启动的时候,卡在了初次调度切换不到initcode这边,没有调用exec。没有panic,似乎只在死循环。我也实在想不出是什么原因,最后把代码删了【悲】想想我应该用git保存一下改前改后的。这下实在是难受了,我的想法也暂时没有机会实践了。等到明年大三说不定还得再交一次这玩意,到时候再探究探究吧hhh
-发现这个最后没成还改了半天的我最后非常沮丧地去看了hints【又一心浮气躁耐心不足的表现,但确实绷不住了】,发现它居然说只用修改三个地方:fork、exec以及sbrk。
-我把kernel/下的每个文件都搜了一遍,发现确实,只有这三个,以及proc.c,vm.c,涉及到对页表项的增删。而在用户态中,想要对进程的内存进行管理,似乎只能通过系统调用sbrk。而proc.c和vm.c中确实没什么好改的。因为里面增加的映射,都是trapframe、trampoline、inicode这种不会一般在copyin中用到的虚拟地址。所以,要改的地方,确确实实,只有fork、exec以及sbrk。
+考虑到这样一个进程:
+进程使用页表时,需要将整个页表读入内存。
+如果使用单级页表,尽管一个进程仅使用到页表中的某两项,也需要把整个页表都读入内存,光是页表就占据了2^15x7x4k/2^20 约为1G的内存空间。
+如果使用三级页表,一个进程需要用到某两页。假设这两页存储在不同的二级页表中,则只需要读入1+2+2=5页 约为20K的内存空间。
+两者相对比,显然用三级页表比单级页表顶多了。三级页表相较于一级页表,多用了13%的物理空间,却可以节省99.998%的空间。
+每个进程会保留自己的一份用户级别的页表地址。当轮到自己使用CPU时,会将CPU的satp寄存器更换为自己的页表地址。
+介绍了xv6中内核的页表结构。
--Xv6 applications ask the kernel for heap memory using the sbrk() system call.
+这里为了方便,就把三级页表省略了,只留下va和pa的对比
很悲伤,我的初见思路是错误的()
-而这三个地方的共同点,就是都会对页表进行大量的copy。
-//in proc.c fork() |
//in exec.c |
//in syscall.c |
每个进程都有一个用户级别的页表。xv6给内核提供了一个单独的内核地址空间的页表。其层级映射关系如下:
+在kernel/memlayout.h中正记录了这些参数:
+// Physical memory layout |
所以,我们要做的事情很简单:写一个坐收渔翁之利的函数,内容为把一个页表的所有内容复制到另一个页表。然后再在这几个地方调用这个函数即可。
---注意:由于我写得实在是太烦了,已经思考不下去了。为了放过我自己,我写了个虽然能过得去测试但是其实毛病重重的代码。垃圾点为以下几点:
+由图可知,一直从0x0到0x86400000,都是采取的直接映射的方式,虚拟地址=物理地址,这段是内核使用的空间。在0x0-0x800000000阶段,物理地址代表着各种IO设备的存储器。
+但是注意,在0x86400000(PHYSTOP)以上的地址都不是直接映射,这些非直接映射的层级包含两类:
-
-需要去掉freewalk中的panic
-我的kvmcopy的实现是,user pagetable(下面简称up)和tp的相同虚拟地址共用同一页物理内存。也就是说,页表不一样,但所指向的物理内存是同一个。这样设计的目的是为了能够让tp及时用到up的更新后的数据。
-这会导致啥呢?在进程释放时,需要一起调用
+proc_freepagetable
和proc_freekpgtbl
。proc_freepagetable
调用完后,所指向的那堆物理内存已经寄完了,如果再调用proc_freekpgtbl
,显然,就会发生页表未释放但页表对应内存已经释放的问题,freewalk就会panic。因此,我简单粗暴地直接把freewalk的panic删掉了【抖】也许有别的解决方法,但我真是烦得不想想了放过我吧(- -
trampoline
+++It is mapped at the top of the virtual address space; user page tables have this same mapping.
+它有一点很特殊的是,它实际对应的物理内存是0x80000000开始的一段。也就是说,0x80000000开始的这段内存,既被直接映射了,也被trampoline通过虚拟地址映射了。它被映射了两次。
好像暂时没有第二点了()
+内核栈
++Each process has its own kernel stack, which is mapped high so that below it xv6 can leave an unmapped guard page. The guard page’s PTE is invalid (i.e., PTE_V is not set), so that if the kernel overflflows a kernel stack, it will likely cause an exception and the kernel will panic.
+guard page可以用来防止内核栈溢出。
+
// in vm.c |
// in proc.c fork() |
内核使用PTE_R和PTE_X权限映射trampoline和kernel text。这表明这份内存段可以读,可以被当做指令块执行,但不能写。其他的块都是可读可写的,除了guard page被设置为不可访问。
+操作地址空间和页表部分的代码都在kernel/vm.c
中。代表页表的数据结构是pagetable_t
。
vm.c的主要函数有walk、mappages等。walk用来在三级页表中找到某个虚拟地址表项,或者创建一个新的表项。mappages用来新建一个表项,主要用到了walk函数。
+vm.c中,以kvm开头的代表操纵内核页表,以uvm开头的代表操纵进程里的用户页表。
+一开始操作系统初始化时,会调用vm.c中的kvminit来创建内核页表。主要就是在以内核地址空间的页表结构在填写页表。
+void |
// in exec.c |
其中,kvmmap用来在内核页表中添加一个新的表项。其函数形式为
+// add a mapping to the kernel page table. |
uint64 |
实现主要逻辑的是mappages函数
+// Create PTEs for virtual addresses starting at va that refer to |
// in proc.c |
通过虚拟地址获取表项主要是通过walk实现的
+// Return the address of the PTE in page table pagetable |
--这一步不能忽视,因为内核启动的时候就需要用到copyinstr。
-
// in proc.c userinit() |
使用的是kvminithart函数。它将内核页表的root page table的物理地址写入了satp寄存器。从这个函数之后,就开启了内存映射。
+// Switch h/w page table register to the kernel's page table, |
// in vm.c freewalk() |
traps=系统调用+异常+中断。本章着重讲traps概述以及traps中的系统调用。
-对trap的处理包含四个部分:硬件处理、中断向量、trap handler、对应的处理函数
---
-
在RISC-V中,异常通常是由于程序执行过程中的错误或非预期事件而引起的,包括故障(faults)、陷阱(traps)和中止(aborts)。中断(interrupts)则是由外部事件触发的,例如定时器到期、外部设备请求等。中断是异步事件,与当前正在执行的指令无关,因此会在任何时候发生。
-xv6是基于RISC-V架构的。因此,发生异常的时候,就会跳转到统一的kernel trap,然后再在里面通过读取scause来进行相应处理。
-发生中断的处理方式就和x86差不多了,都是通过中断向量实现的。
+其中sfence_vma()的用途是强制更新TLB的旧页表,类似于Java volatile的作用。
+疑问
附上书里的详细解释:
++
TLB与页表类似于cache与主存的关系。TLB保存了页表的一部分。
+我的错误想法
我怎么感觉怪怪的啊?因为TLB既然是高速缓存,那么读写页表也应该优先从TLB读写【注:应该就是从这里开始错的hhh写应该是直接写入页表】。所以说,会陈旧的应该是主存中的页表,而不是TLB中的页表。但是,书里是说,改完页表必须通知TLB更改。也就是说,读写页表不是从TLB读写的,那该是从哪里?是TLB以外的free memory吗?
+不过,要是从多CPU的角度思考,说不定他这个意思是某个CPU的TLB变了,需要通知其他所有CPU的TLB也变。虽然不同CPU当前执行的进程是不一样的,使用的页表项不一样,切换进程的时候也会把用户地址空间的页表项flush掉。但是内核地址空间的页表项一般是不会随着进程切换而flush掉的。所以内核页表修改就需要手动多CPU同步。
+我认为多CPU角度考虑更加合理,因为它最后说了,xv6会在内核页表init后flush,以及在从内核态切换回用户态的时候flush。这两个(好像)都影响内核页表比较多,所以就需要手动flush一下。
+解答
之后学了缺页异常后,可以发现这里其实是没问题的。
+ ++
页表的管理(创建、更新、删除等)是由操作系统负责的。地址转换时,页表检索是由硬件内存管理单元(Memory Management Unit, MMU)负责的。MMU通常由两部分构成:表查找单元(Table Walk Unit, TWU)和转换旁路缓冲(Translation Lookaside Buffer, TLB)[2]。TWU负责链式的访问PDE、PTE,完成上述的查表过程。
+应用多级页表之后,想要完成一次地址转换,需要访问多级目录和页表,这么多次的内存访问会严重降低性能。
+为了优化地址转换速度,人们在MMU中增加了一块高速cache,专门用来缓存虚拟地址到物理地址的映射,这块cache就是TLB[7][8]。MMU在做地址转换的时候,会先检索TLB,如果命中则直接返回对应的物理地址,如果不命中则会调用TWU查找页表。
+TLB中缓存的是虚拟地址到物理地址映射。然而,多级页表的查找是一个链式的过程,对于在虚拟地址空间中连续的两个页,它们的各级目录项可能都是一样的,只有最后一级页号不一样。查找完第一个虚拟页之后,我们可以将相同的前级目录项都缓存起来。查找第二个虚拟页时,可以直接使用缓存好的前几级目录项,节省查找时间。这种缓存叫做Page Structure Cache[9]。
+而当TLB和MMU中都没有该物理页,就会发生缺页异常。但是操作系统仅会对页表更新,而不会被TLB更新。故而,TBL中数据可能陈旧,需要手动flush。
+Physical memory allocation
在内核运行的时候,需要申请很多空间用来存放各种数据。
++-The kernel must allocate and free physical memory at run-time for page tables, user memory, kernel stacks, and pipe buffers.
control register
risc-v为trap提供了一组寄存器:
--
+- -
stvec
-trap handler的入口地址
-- -
sepc
-原程序PC
-- -
scause
-中断号
-- -
sscratch
-TRAPFRAME地址
-- -
sstatus
-是否允许中断,以及中断来自内核态还是用户态
-用的是这段空闲内存:
+
--The above registers relate to traps handled in supervisor mode, and they cannot be read or written in user mode.
-There is an equivalent set of control registers for traps handled in machine mode; xv6 uses them only for the special case of timer interrupts.
+It keeps track of which pages are free by threading a linked list through the pages themselves.
每个CPU都有自己的一套这样的控制寄存器。
-硬件处理步骤
时钟中断、device interrupt以及关中断的情况下,不会做以下步骤。
+kalloc.c中就是这么实现的。
+Code: Physical memory allocator
内核运行时申请释放空闲物理空间是通过
kernel/kalloc.c
完成的。它为内核栈、用户进程、页表和管道buffer服务。--\1. If the trap is a device interrupt, and the sstatus SIE bit is clear, don’t do any of the following.
-\2. Disable interrupts by clearing SIE.关中断
-\3. Copy the pc to sepc.保存PC
-\4. Save the current mode (user or supervisor) in the SPP bit in sstatus.保存mode
-\5. Set scause to reflflect the trap’s cause.保存中断号
-\6. Set the mode to supervisor.切换到内核态
-\7. Copy stvec to the pc.将trap handler写入pc,开始执行trap handler【uservec or kernelvec?】
+kalloc.c用来在运行时申请分配新的一页,上面的vm.c正是用了kalloc申请一页,要么作为页表,要么作为存储数据的第三级页表指向的物理内存。
切换到内核页表、切换内核栈、保存寄存器现场这些工作交给操作系统完成。
-Traps from user space
从用户态来的trap会经历怎么样的过程?
-前面说到,下面需要进行页表的切换,页表的切换必然是接下来要做的指令的某个环节。那么为了让页表切换之后,CPU还知道要从哪里取指执行,就要让某段物理内存在内核空间和用户空间的虚拟地址一样。这样,不论页表是用户的还是内核的,都可以通过同样的虚拟地址访问到该段存放指令的物理内存从而继续执行。
-这段虚拟地址就是trampoline。它在内核页表和用户页表都位于MAXVA的位置。
+最后应该会在空闲内存内形成这样的结构:
+内存分成一页一页的,每页内存中的前几个字节存储着其对应队列中下一块内存的物理地址。不一定是从小地址到大地址顺序连接。
--我感觉这段大概可以这么理解:
-通过查看代码,可知trampoline段实际上存储的是trampoline.S中的数据,也即uservec和userret的汇编代码,也即执行切换页表我们实际上就是在执行trampoline里的代码。trampoline的存在,就可以使得每个页表的这部分都是这两个的代码,这样一来切换页表也就不影响指令流的执行。
+It store each free page’s run structure in the free page itself, since there’s nothing else stored there.
-
stevc存储的正是trampoline段中的uservec。
-uservec
sscratch里面存的是trapframe的值。
-trapframe存在于用户空间中,并且每个进程的trapframe所处位置固定是在trampoline下方。
--
首先将寄存器的值都存入trapframe中;然后,再从trapframe中读取内核栈指针、当前CPUid,下一步要跳转的usertrap的地址,以及内核页表。最后,uservec切换到内核页表,并且jmp到usertrap。
-+
#in kernel/trampoline.S
.section trampsec
.globl trampoline
trampoline:
.align 4
.globl uservec
uservec:
#
# trap.c sets stvec to point here, so
# traps from user space start here,
# in supervisor mode, but with a
# user page table.
#
# sscratch points to where the process's p->trapframe is
# mapped into user space, at TRAPFRAME.
#
# swap a0 and sscratch
# so that a0 is TRAPFRAME
csrrw a0, sscratch, a0
# save the user registers in TRAPFRAME
sd ra, 40(a0)
sd sp, 48(a0)
# ...
sd t6, 280(a0)
# save the user a0 in p->trapframe->a0
csrr t0, sscratch
sd t0, 112(a0)
# restore kernel stack pointer from p->trapframe->kernel_sp
# 完成了内核栈的切换
ld sp, 8(a0)
# make tp hold the current hartid, from p->trapframe->kernel_hartid
ld tp, 32(a0)
# load the address of usertrap(), p->trapframe->kernel_trap
ld t0, 16(a0)
# restore kernel page table from p->trapframe->kernel_satp
ld t1, 0(a0)
# 这里完成了页表的切换
csrw satp, t1
sfence.vma zero, zero
# a0 is no longer valid, since the kernel page
# table does not specially map p->tf.
# jump to usertrap(), which does not return
jr t0+
// Physical memory allocator, for user processes,
// kernel stacks, page-table pages,
// and pipe buffers. Allocates whole 4096-byte pages.
// 释放在这范围内的物理内存空间
void freerange(void *pa_start, void *pa_end);
// 也就是上面说的free memory的起始位置
extern char end[]; // first address after kernel.
// defined by kernel.ld.
// run代表的是一页内存
struct run {
struct run *next;
};
// 代表了整个内核空闲的物理空间
struct {
struct spinlock lock;
struct run *freelist;
} kmem;
void
kinit()
{
initlock(&kmem.lock, "kmem");
// init的时候先清空空闲空间,建立空闲页队列
freerange(end, (void*)PHYSTOP);
}
void
freerange(void *pa_start, void *pa_end)
{
char *p;
// PGROUNDUP和PGROUNDDOWN是用于将地址四舍五入到PGSIZE
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE)
kfree(p);
}
// Free the page of physical memory pointed at by pa,
// which normally should have been returned by a
// call to kalloc(). (The exception is when
// initializing the allocator; see kinit above.)
void
kfree(void *pa)
{
struct run *r;
// pa得是整数页,并且得在内核物理内存范围之间
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP)
panic("kfree");
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
// 之后将在pa对应的那一页的前几个字节写入next字段
r = (struct run*)pa;
// 这意思就是在空闲内存的链表队列中新增一块
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}
// Allocate one 4096-byte page of physical memory.
// Returns a pointer that the kernel can use.
// Returns 0 if the memory cannot be allocated.
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}Process address space
当用户进程叫xv6分配内存时,xv6会用kalloc去取,然后登记在页表上。
--注:trampoline和trapframe有一些相通点。
-trampoline为了保障某段物理内存的虚拟地址在内核栈和用户栈中不变,做出的努力是,在内核栈和用户栈都分配同一位置的PTE。
-trapframe用于保护现场、用户态向内核态传递参数等等,做出的努力是,在用户栈分配同一位置的PTE,在内核态的局部变量中保存了自己的物理地址。
-这两个说实话有点容易混起来,因为我想了半天trampoline可不可以用类似trapframe一样的方法,结论是不行。因为你trampoline的作用是维持指令序列依然不变,不会突然没掉;而trapframe段是用来存储数据而非执行的,对其的控制也是需要指令的。如果trampoline使用第二种方法,指令流就会断掉,更别说别的了。
+The stack is a single page, and is shown with the initial contents as created by exec. Strings containing the command-line arguments, as well as an array of pointers to them, are at the very top of the stack. Just under that are values that allow a program to start at main as if the function main(argc, argv) had just been called.
usertrap
作用是得到trap发生的原因,并且执行对应的处理程序,然后返回结果。
-- -
// handle an interrupt, exception, or system call from user space.
// called from trampoline.S
void
usertrap(void)
{
int which_dev = 0;
if((r_sstatus() & SSTATUS_SPP) != 0)
panic("usertrap: not from user mode");
// send interrupts and exceptions to kerneltrap(),
// since we're now in the kernel.
//首先把trap handler切换到kernel的,这样一来如果在kernel中发生trap就会由kernel的handler处理
w_stvec((uint64)kernelvec);
struct proc *p = myproc();
//在当前进程中再次保存用户程序的原PC,防止之后sepc被覆盖
// save user program counter.
p->trapframe->epc = r_sepc();
//根据cause号不同处理
if(r_scause() == 8){
// system call
if(p->killed)
exit(-1);
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;
// an interrupt will change sstatus &c registers,
// so don't enable until done with those registers.
//注意,在此处开启了中断
intr_on();
//调用syscall处理
syscall();
} else if((which_dev = devintr()) != 0){
// ok
} else {
printf("usertrap(): unexpected scause %p pid=%d\n", r_scause(), p->pid);
printf(" sepc=%p stval=%p\n", r_sepc(), r_stval());
p->killed = 1;
}
if(p->killed)
exit(-1);
// give up the CPU if this is a timer interrupt.
if(which_dev == 2)
yield();
usertrapret();
}执行对应的处理函数
比如说system call会修改trapframe中的a0为返回的结果,会获取trapframe中的各个参数。这个“保护现场“感觉是非常微妙的,它兼顾了保护现场和传递参数两个作用
-usertrapret
回到用户态。之前陷入内核态对stvec、satp、sp、hartid、trap handler都做了适应内核态的改变,因而这里就需要改回原来适应用户态的样子,然后返回用户态。
-+
// return to user space
void
usertrapret(void)
{
struct proc *p = myproc();
// 关中断
// we're about to switch the destination of traps from
// kerneltrap() to usertrap(), so turn off interrupts until
// we're back in user space, where usertrap() is correct.
intr_off();
// 重置trapframe
// send syscalls, interrupts, and exceptions to trampoline.S
w_stvec(TRAMPOLINE + (uservec - trampoline));
// set up trapframe values that uservec will need when
// the process next re-enters the kernel.
p->trapframe->kernel_satp = r_satp(); // kernel page table
p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack
p->trapframe->kernel_trap = (uint64)usertrap;
p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()
// set up the registers that trampoline.S's sret will use
// to get to user space.
// set S Previous Privilege mode to User.
unsigned long x = r_sstatus();
x &= ~SSTATUS_SPP; // clear SPP to 0 for user mode
x |= SSTATUS_SPIE; // enable interrupts in user mode
w_sstatus(x);
// set S Exception Program Counter to the saved user pc.
w_sepc(p->trapframe->epc);
// tell trampoline.S the user page table to switch to.
uint64 satp = MAKE_SATP(p->pagetable);
// jump to trampoline.S at the top of memory, which
// switches to the user page table, restores user registers,
// and switches to user mode with sret.
uint64 fn = TRAMPOLINE + (userret - trampoline);
((void (*)(uint64,uint64))fn)(TRAPFRAME, satp);
}+
Code: sbrk
++Sbrk is the system call for a process to shrink or grow its memory. The system call is implemented by the function growproc (kernel/proc.c:239).
+-
// Grow or shrink user memory by n bytes.注意单位是bytes,grow n+,shrink n-
// Return 0 on success, -1 on failure.
// 主要逻辑还是通过vm.c实现
int
growproc(int n)
{
uint64 sz;//size
struct proc *p = myproc();
sz = p->sz;
if(n > 0){
if((sz = uvmalloc(p->pagetable, sz, sz + n, PTE_W)) == 0) {
return -1;
}
} else if(n < 0){
sz = uvmdealloc(p->pagetable, sz, sz + n);
}
p->sz = sz;
return 0;
}userret
+
.globl userret
userret:
# userret(TRAPFRAME, pagetable)
# switch from kernel to user.
# usertrapret() calls here.
# a0: TRAPFRAME, in user page table.
# a1: user page table, for satp.
# 切换为用户页表
# switch to the user page table.
csrw satp, a1
sfence.vma zero, zero
# put the saved user a0 in sscratch, so we
# can swap it with our a0 (TRAPFRAME) in the last step.
ld t0, 112(a0)
csrw sscratch, t0
# restore all but a0 from TRAPFRAME
ld ra, 40(a0)
ld sp, 48(a0)
# ...
ld t6, 280(a0)
# restore user a0, and save TRAPFRAME in sscratch
csrrw a0, sscratch, a0
# return to user mode and user pc.
# usertrapret() set up sstatus and sepc.
sret-
// Allocate PTEs and physical memory to grow process from oldsz to
// newsz, which need not be page aligned.不需要页对齐 Returns new size or 0 on error.
uint64
uvmalloc(pagetable_t pagetable, uint64 oldsz, uint64 newsz, int xperm)
{
char *mem;
uint64 a;
if(newsz < oldsz)
return oldsz;
// oldsz向上取整
oldsz = PGROUNDUP(oldsz);
// 每页alloc
for(a = oldsz; a < newsz; a += PGSIZE){
mem = kalloc();
if(mem == 0){
// 说明失败,恢复到原状
// 这里不用像下面一样kfree是因为这里压根没有alloc成功
uvmdealloc(pagetable, a, oldsz);
return 0;
}
// 除去junk data
memset(mem, 0, PGSIZE);
// 放入页表
if(mappages(pagetable, a, PGSIZE, (uint64)mem, PTE_R|PTE_U|xperm) != 0){
// 不成功
// dealloc原理是顺着页表一个个free的。由于mem此处没有成功放入页表,所以就得单独free掉
kfree(mem);
uvmdealloc(pagetable, a, oldsz);
return 0;
}
}
return newsz;
}Code: calling system calls
-Chapter 2 ended with initcode.S invoking the exec system call (user/initcode.S:11). Let’s look at how the user call makes its way to the exec system call’s implementation in the kernel.要讲如何从用户态找到exec的代码了。
+Code:exec
+-Exec is the system call that creates the user part of an address space. It initializes the user part of an address space from a fifile stored in the fifile system.
+
exec
是创建地址空间的用户部分的系统调用。它使用一个存储在文件系统中的文件初始化地址空间的用户部分。Code: system call arguments
讲的是系统调用时,是如何把用户态传递的地址转化为内核态地址的。
-这个部分可以看看hit实验的实验3 6.7,讲得很详细,而且流程是差不多的。linux0.11的
-get_fs_byte()
就相当于xv6的copyinstr
。Traps from kernel space
kernelvec
不同于用户态还得先潜入内核再潜出内核,内核的trap可简单多了,省去了切来切去各种东西的步骤,只需当做一个普通的函数调用就行。
-+
# in kernel/kernelvec.S
.globl kerneltrap
.globl kernelvec
.align 4
kernelvec:
// make room to save registers.
addi sp, sp, -256
// save the registers.
sd ra, 0(sp)
sd sp, 8(sp)
# ...
sd t6, 240(sp)
// call the C trap handler in trap.c
call kerneltrap
// restore registers.
ld ra, 0(sp)
ld sp, 8(sp)
# ...
ld t5, 232(sp)
ld t6, 240(sp)
addi sp, sp, 256
// return to whatever we were doing in the kernel.
sret-
int
exec(char *path, char **argv)
{
char *s, *last;
int i, off;
uint64 argc, sz = 0, sp, ustack[MAXARG], stackbase;
struct elfhdr elf;
struct inode *ip;
struct proghdr ph;
pagetable_t pagetable = 0, oldpagetable;
struct proc *p = myproc();
//开始打开文件的意思吧(
begin_op();
//ip是一个inode
//打开路径为path的文件
if((ip = namei(path)) == 0){
end_op();
return -1;
}
//暂时锁住文件,别人不许动
ilock(ip);
//之后应该就是把文件读入内存吧
// Check ELF header
if(readi(ip, 0, (uint64)&elf, 0, sizeof(elf)) != sizeof(elf))
goto bad;
if(elf.magic != ELF_MAGIC)
goto bad;
//分配新页表
if((pagetable = proc_pagetable(p)) == 0)
goto bad;
//elfhd应该指的是可执行文件头
// Load program into memory.
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
goto bad;
if(ph.type != ELF_PROG_LOAD)
continue;
if(ph.memsz < ph.filesz)
goto bad;
if(ph.vaddr + ph.memsz < ph.vaddr)
goto bad;
if(ph.vaddr % PGSIZE != 0)
goto bad;
//总之顺利读到了
uint64 sz1;
//读到了就给它分配新空间并且填入页表
if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0)
goto bad;
sz = sz1;
if(loadseg(pagetable, ph.vaddr, ip, ph.off, ph.filesz) < 0)
goto bad;
}
//在这里解锁
iunlockput(ip);
end_op();
ip = 0;
p = myproc();
uint64 oldsz = p->sz;
//读完文件,开始造一个新的用户栈【fork之后用户栈是不会清空的】
// Allocate two pages at the next page boundary.
// Make the first inaccessible as a stack guard.
// Use the second as the user stack.
sz = PGROUNDUP(sz);
uint64 sz1;
if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE, PTE_W)) == 0)
goto bad;
sz = sz1;
// mark a PTE invalid for user access.造guard page
uvmclear(pagetable, sz-2*PGSIZE);
// sp为栈顶
sp = sz;
// 应该指的是栈尾
stackbase = sp - PGSIZE;
// 开始往栈中填入执行参数
// Push argument strings, prepare rest of stack in ustack.
for(argc = 0; argv[argc]; argc++) {
if(argc >= MAXARG)
goto bad;
sp -= strlen(argv[argc]) + 1;
sp -= sp % 16; // riscv sp must be 16-byte aligned
if(sp < stackbase)
goto bad;
//argv来自用户空间,所以需要使用copyout
if(copyout(pagetable, sp, argv[argc], strlen(argv[argc]) + 1) < 0)
goto bad;
//这什么东西
//exec一次将参数中的一个字符串复制到栈顶,并在ustack中记录指向它们的指针
ustack[argc] = sp;
}
//放置空指针
ustack[argc] = 0;
// push the array of argv[] pointers.
sp -= (argc+1) * sizeof(uint64);
sp -= sp % 16;
if(sp < stackbase)
goto bad;
if(copyout(pagetable, sp, (char *)ustack, (argc+1)*sizeof(uint64)) < 0)
goto bad;
// arguments to user main(argc, argv)
// argc is returned via the system call return
// value, which goes in a0.
p->trapframe->a1 = sp;
// Save program name for debugging.
for(last=s=path; *s; s++)
if(*s == '/')
last = s+1;
safestrcpy(p->name, last, sizeof(p->name));
//只有成功了才会来到这,才会覆盖掉旧的内存镜像
// Commit to the user image.
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
p->trapframe->epc = elf.entry; // initial program counter = main
p->trapframe->sp = sp; // initial stack pointer
proc_freepagetable(oldpagetable, oldsz);
return argc; // this ends up in a0, the first argument to main(argc, argv)
bad:
//释放新镜像,不改变旧镜像
if(pagetable)
proc_freepagetable(pagetable, sz);
if(ip){
iunlockput(ip);
end_op();
}
return -1;
}kerneltrap
-kerneltrap is prepared for two types of traps: device interrrupts and exceptions.
-It calls devintr (kernel/trap.c:177) to check for and handle the former. If the trap isn’t a device interrupt, it must be an exception, and that is always a fatal error if it occurs in the xv6 kernel; the kernel calls panic and stops executing.
-If kerneltrap was called due to a timer interrupt, and a process’s kernel thread is running (rather than a scheduler thread), kerneltrap calls yield to give other threads a chance to run.
+Real world
+
xv6内核缺少一个类似
+malloc
可以为小对象提供内存的分配器,这使得内核无法使用需要动态分配的复杂数据结构。【确实,感觉一分配就是一页(】内存分配是一个长期的热门话题,基本问题是有效使用有限的内存并为将来的未知请求做好准备。今天,人们更关心速度而不是空间效率。此外,一个更复杂的内核可能会分配许多不同大小的小块,而不是(如xv6中)只有4096字节的块;一个真正的内核分配器需要处理小分配和大分配。
+Lab:Pagetable
+-In this lab you will explore page tables and modify them to to speed up certain system calls and to detect which pages have been accessed.
- -
// in kernel/trap.c
// interrupts and exceptions from kernel code go here via kernelvec,
// on whatever the current kernel stack is.
void
kerneltrap()
{
int which_dev = 0;
uint64 sepc = r_sepc();
uint64 sstatus = r_sstatus();
uint64 scause = r_scause();
if((sstatus & SSTATUS_SPP) == 0)
panic("kerneltrap: not from supervisor mode");
if(intr_get() != 0)
panic("kerneltrap: interrupts enabled");
// 在此处的devintr对不同的设备进行不同的处理方式
if((which_dev = devintr()) == 0){
printf("scause %p\n", scause);
printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
panic("kerneltrap");
}
// give up the CPU if this is a timer interrupt.
if(which_dev == 2 && myproc() != 0 && myproc()->state == RUNNING)
yield();
// the yield() may have caused some traps to occur,
// so restore trap registers for use by kernelvec.S's sepc instruction.
w_sepc(sepc);
w_sstatus(sstatus);
}Page-fault exceptions
似乎xv6是没有这个缺页exception的。这里主要讲解了三个可以利用缺页中断实现的优化:COW fork、lazy allocation、paging from disk。还提及了automatically extending stacks 以及memory-mapped fifiles。
-Lab:Trap
-This lab explores how system calls are implemented using traps. You will first do a warm-up exercises with stacks and then you will implement an example of user-level trap handling.
+不过遗憾的是usertests还有好几个没通过,具体都标注了。
+Speed up system calls
+-When each process is created, map one read-only page at USYSCALL (a VA defined in
+memlayout.h
). At the start of this page, store astruct usyscall
(also defined inmemlayout.h
), and initialize it to store the PID of the current process. For this lab,ugetpid()
has been provided on the userspace side and will automatically use the USYSCALL mapping. You will receive full credit for this part of the lab if theugetpid
test case passes when runningpgtbltest
.RISC-V assembly
题目和答案
-参考:
- -There is a file
-user/call.c
in your xv6 repo. make fs.img compiles it and also produces a readable assembly version of the program inuser/call.asm
.Read the code in call.asm for the functions
+g
,f
, andmain
. Here are some questions that you should answer:感想
乌龙
这里好像是因为实验改版了,我下的是2020年的实验包,在memlayout压根找不到USYSCALL和struct usyscall这俩东西。最后翻了下网上的总算找到了。
+我一开始没找到,还以为USYSCALL以及usyscall这两个都得自己写在memlayout里面,想了很久都没想出来USYSCALL的值应该设置为多少。我认为只需满足两个条件即可:1.所处内存段应该是free memory那段,也即自kernel结束(PHYSTOP)到MAXVA这一大块。2.得确保能被用户和内核都能访问到。
+前者意为虚拟地址在MAXVA和PHYSTOP之间,后者意为那段内存应该标记为PTE_U。这个范围是很宽泛的,我实在不知道要分配这期间的哪块内存,感觉也不大可能是真的自由度那么大。所以我就偷偷看了hints【悲】,想看它对这个USYSCALL应该写什么值有没有建议。结果发现这东西是实验给我们定的。遂去网上找到了它给的真正的USYSCALL值。
++ +
struct usyscall{
int pid;
};用户的ugetpid只找到了一个截图:
++
恕我愚钝实在不知道该把这段代码放在哪orz于是接下来写的东西就没有自测。
+panic:freewalk leaf
一开始写好代码准备启动xv6的时候爆出了这么一个panic,搜了一下得到如下解答:
+++来源:MIT-6.S081-2020实验(xv6-riscv64)十:mmap
+这时运行会发现freewalk函数panic:
freewalk: leaf
,这是因为freewalk希望所有虚拟地址已经被解绑并释放对应的物理空间了,该函数只负责释放页表。让我得知freewalk在vm.c下面【吐槽,我一开始还以为是自由自在地走(,看到这个才反应过来是free walk,跟页表有关的】。结合freewalk的代码
++
可以知道,造成这个panic的原因是需要手动释放页表项。而在这里
++ +
// in proc.c freeproc()
if(p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0;仅仅是释放掉了对应的物理页,页表项并没有被释放。
+对比了一下别人写的,才发现原来这里也需要修改:
++ +
// Free a process's page table, and free the
// physical memory it refers to.
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
//添加此句
uvmunmap(pagetable, USYSCALL, 1, 0);
uvmfree(pagetable, sz);
}这样一来,问题就解决了。
+总结
因而,可以看到,如果进程想使用页的话,需要经历以下四步:
-
+- -
a2
-- -
被inline掉了
-- -
0x64A
--
auipc的作用是把立即数左移12位,低12位补0,和pc相加赋给指定寄存器。这里立即数是0,指定寄存器是ra,即ra=pc=0x30=48。jalr作用是跳转到立即数+指定寄存器处并且把ra的值置为下一条指令。因此jalr会跳转1562+48=1594=0x64A处,观察汇编代码可知确实在000000000000064a处。
-- -
0x38
- -Run the following code.
-+
unsigned int i = 0x00646c72;
printf("H%x Wo%s", 57616, &i);- 通过kalloc获取物理页地址(可以通过该地址对页进行读写),并且记录在进程proc结构中(否则之后就获取不了了)
+- 建立mappages映射
+- 释放物理页
+- 释放PTE映射
+可见12和34都是分别一一对应的。
+代码
-
// Look in the process table for an UNUSED proc.
// If found, initialize state required to run in the kernel,
// and return with p->lock held.
// If there are no free procs, or a memory allocation fails, return 0.
static struct proc*
allocproc(void)
{
struct proc *p;
//有线程池那味了
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == UNUSED) {
goto found;
} else {
release(&p->lock);
}
}
return 0;
found:
p->pid = allocpid();
// Allocate a trapframe page.
if((p->trapframe = (struct trapframe *)kalloc()) == 0){
release(&p->lock);
return 0;
}
// Allocate a usyscall page.
if((p->usyscall = (struct usyscall *)kalloc()) == 0){
release(&p->lock);
return 0;
}
//在USYSCALL写入usyscall结构体
p->usyscall->pid = p->pid;
// An empty user page table.
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
// Set up new context to start executing at forkret,
// which returns to user space.
memset(&p->context, 0, sizeof(p->context));
p->context.ra = (uint64)forkret;
p->context.sp = p->kstack + PGSIZE;
return p;
}
// free a proc structure and the data hanging from it,
// including user pages.
// p->lock must be held.
static void
freeproc(struct proc *p)
{
if(p->trapframe)
kfree((void*)p->trapframe);
p->trapframe = 0;
if(p->pagetable)
proc_freepagetable(p->pagetable, p->sz);
p->pagetable = 0;
if(p->usyscall)
kfree((void*)p->usyscall);
p->usyscall = 0;
p->sz = 0;
p->pid = 0;
p->parent = 0;
p->name[0] = 0;
p->chan = 0;
p->killed = 0;
p->xstate = 0;
p->state = UNUSED;
}
// Create a user page table for a given process,
// with no user memory, but with trampoline pages.
pagetable_t
proc_pagetable(struct proc *p)
{
pagetable_t pagetable;
// An empty page table.
pagetable = uvmcreate();
if(pagetable == 0)
return 0;
// map the trampoline code (for system call return)
// at the highest user virtual address.
// only the supervisor uses it, on the way
// to/from user space, so not PTE_U.
if(mappages(pagetable, TRAMPOLINE, PGSIZE,
(uint64)trampoline, PTE_R | PTE_X) < 0){
uvmfree(pagetable, 0);
return 0;
}
// map the trapframe just below TRAMPOLINE, for trampoline.S.
if(mappages(pagetable, TRAPFRAME, PGSIZE,
(uint64)(p->trapframe), PTE_R | PTE_W) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
// 映射USYSCALL
if(mappages(pagetable, USYSCALL, PGSIZE,
(uint64)(p->usyscall), PTE_R|PTE_U) < 0){
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, 0);
return 0;
}
return pagetable;
}
// Free a process's page table, and free the
// physical memory it refers to.
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmunmap(pagetable, USYSCALL, 1, 0);
uvmfree(pagetable, sz);
}What is the output? Here’s an ASCII table that maps bytes to characters.
-The output depends on that fact that the RISC-V is little-endian. If the RISC-V were instead big-endian what would you set
-i
to in order to yield the same output? Would you need to change57616
to a different value?+
问答题
+- -Which other xv6 system call(s) could be made faster using this shared page? Explain how.
- - 取决于寄存器a2(第3个参数)的值。
-Backtrace
-For debugging it is often useful to have a backtrace: a list of the function calls on the stack above the point at which the error occurred.
--
The compiler puts in each stack frame a frame pointer that holds the address of the caller’s frame pointer. Your
+backtrace
should use these frame pointers to walk up the stack and print the saved return address in each stack frame.我觉得如果能在fork的父子进程用shared page共享页表应该会节省很多时间和空间,用个读时写。其他的倒是想不到了。不过这题会不会问的是那些在内核态和用户态穿梭频繁的system call呢?这个的话我就想不出来了。
+Print a page table
+-write a function that prints the contents of a page table.
+Define a function called
+vmprint()
.It should take a
+pagetable_t
argument, and print that pagetable in the format described below.Insert
+if(p->pid==1) vmprint(p->pagetable)
in exec.c just before thereturn argc
, to print the first process’s page table.+
The first line displays the argument to
+vmprint
. After that there is a line for each PTE, including PTEs that refer to page-table pages deeper in the tree. Each PTE line is indented by a number of" .."
that indicates its depth in the tree.Each PTE line shows the PTE index in its page-table page, the pte bits, and the physical address extracted from the PTE. Don’t print PTEs that are not valid.
+In the above example, the top-level page-table page has mappings for entries 0 and 255. The next level down for entry 0 has only index 0 mapped, and the bottom-level for that index 0 has entries 0, 1, and 2 mapped.
-Some hints:
--
+- -
Add the prototype for backtrace to
-kernel/defs.h
so that you can invokebacktrace
insys_sleep
.- -
The GCC compiler stores the frame pointer of the currently executing function in the register s0. Add the following function to kernel/riscv.h:
-+
static inline uint64
r_fp()
{
uint64 x;
asm volatile("mv %0, s0" : "=r" (x) );
return x;
}感想
+
很可惜,我在上面检索
+freewalk leaf
到底是什么东西的时候,不小心看到了这题需要去参照freewalk这个提示【悲】其实我觉得这点还是需要绕点弯才能想到的,可能直接想到有点难【谁知道呢,世界线已经变动了】。它这个打印页表其实最主要是考查如何遍历页表,这让人想起了walk这样的东西。但是walk是根据虚拟地址一级级找PTE的,中间很多地方会被跳过。有没有一个过程会在做事的时候遍历整个页表呢?答案是,这个过程就是释放页表的过程。释放页表才会一个个地看是否需要释放。释放页表的函数是freewalk,因而这道题参考freewalk的代码即可。
+我觉得从“遍历页表”联想到“释放页表”这点是很巧的。不过也不会很突兀,毕竟学数据结构时就知道释放就需要遍历,逆向思维有点难但问题不大。
+其他的就都挺简单的,不多赘述。
+代码
记得在defs.h中添加声明
++ +
//在vm.c下
void
vmprint_helper(pagetable_t pagetable,int level)
{
// there are 2^9 = 512 PTEs in a page table.
for(int i = 0; i < 512; i++){
pte_t pte = pagetable[i];
if(pte & PTE_V){
for(int j=0;j<level;j++){
printf(" ..");
}
printf("%d: pte %p pa %p\n",i,(uint64)pte,(uint64)(PTE2PA(pte)));
if((pte & (PTE_R|PTE_W|PTE_X)) == 0){
// this PTE points to a lower-level page table.
uint64 child = PTE2PA(pte);
vmprint_helper((pagetable_t)child,level+1);
}
}
}
}
// 打印页表
void
vmprint(pagetable_t pagetable)
{
// typedef uint64 *pagetable_t;所以pagetable可以以%p形式打印
printf("page table %p\n",(uint64)pagetable);
vmprint_helper(pagetable,1);
}问答题
++Explain the output of
+vmprint
in terms of Fig 3-4 from the text.What does page 0 contain?
+What is in page 2? When running in user mode, could the process read/write the memory mapped by page 1?
+What does the third to last page contain?
+从上面操作系统的启动来看,进程1应该是在main.c中的userinit()中创建的进程,也是shell的父进程。【确实,经实践可得shell的pid为2】
+可以来看一下userint的代码:
++ +
void
userinit(void)
{
struct proc *p;
p = allocproc();
initproc = p;
// 申请一页,将initcode的指令和数据放进去
// allocate one user page and copy initcode's instructions
// and data into it.
/*
uvminit的注释:
// Load the user initcode into address 0 of pagetable,
// for the very first process.
// sz must be less than a page.
*/
uvminit(p->pagetable, initcode, sizeof(initcode));
p->sz = PGSIZE;
//为内核态到用户态的转变做准备
// prepare for the very first "return" from kernel to user.
/*
Trap Frame是指中断、自陷、异常进入内核后,在堆栈上形成的一种数据结构
*/
p->trapframe->epc = 0; // user program counter
p->trapframe->sp = PGSIZE; // user stack pointer
// 修改进程名
safestrcpy(p->name, "initcode", sizeof(p->name));
p->cwd = namei("/");
//这个也许是为了能被优先调度
p->state = RUNNABLE;
release(&p->lock);
}可见,page0是initcode的代码和数据,page1和page2用作了进程的栈,其中page1应该是guard page,page2是stack。
+不过这里从exec的角度解释其实更通用:
+-
int
exec(char *path, char **argv)
{
//分配新页表
if((pagetable = proc_pagetable(p)) == 0)
goto bad;
//elfhd应该指的是可执行文件头
// Load program into memory.
for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
//...
//总之顺利读到了
uint64 sz1;
//读到了就给它分配新空间并且填入页表
if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0)
goto bad;
sz = sz1;
}
//读完文件,开始造一个新的用户栈【fork之后用户栈是不会清空的】
sz = PGROUNDUP(sz);
uint64 sz1;
if((sz1 = uvmalloc(pagetable, sz, sz + 2*PGSIZE, PTE_W)) == 0)
goto bad;
sz = sz1;
// mark a PTE invalid for user access.造guard page
uvmclear(pagetable, sz-2*PGSIZE);
// sp为栈顶
sp = sz;
// 应该指的是栈尾
stackbase = sp - PGSIZE;
//...
}and call this function in backtrace to read the current frame pointer. This function uses in-line assembly to read s0.
-- -
These lecture notes have a picture of the layout of stack frames. Note that the return address lives at a fixed offset (-8) from the frame pointer of a stackframe, and that the saved frame pointer lives at fixed offset (-16) from the frame pointer.
-- -
Xv6 allocates one page for each stack in the xv6 kernel at PAGE-aligned address. You can compute the top and bottom address of the stack page by using
-PGROUNDDOWN(fp)
andPGROUNDUP(fp)
(seekernel/riscv.h
. These number are helpful forbacktrace
to terminate its loop.page0就填程序。这里重点说明一下为什么page1和page2分别是guard page和stack。
+按照它的那个算术关系,stack和guard page的虚拟内存位置关系应该是这样的:
++
那为什么最后在页表中,变成了page1是gurad page,page2是stack这样上下颠倒了呢?看vm.c中的uvmalloc就能明白。
++
在253行设置了新映射。可以看到,这里设置映射的顺序是sz->sz+PGSIZE,也即先设置guard page的映射,再设置stack的映射。所以,这两位才会上下颠倒了。
+Detecting which pages have been accessed
+-Some garbage collectors (a form of automatic memory management) can benefit from information about which pages have been accessed (read or write). In this part of the lab, you will add a new feature to xv6 that detects and reports this information to userspace by inspecting the access bits in the RISC-V page table. The RISC-V hardware page walker marks these bits in the PTE whenever it resolves a TLB miss.
感想
我超,这题真的是那怎能只叫一个拷打……
-存储在s0中的栈帧指针
这个应该是risc-v的约定成俗的特性。我搜了一下risc-v的栈帧指针保存在哪个寄存器,看到了这样一篇文章:
- --+
Your job is to implement
+pgaccess()
, a system call that reports which pages have been accessed.The system call takes three arguments. First, it takes the starting virtual address of the first user page to check. Second, it takes the number of pages to check. Finally, it takes a user address to a buffer to store the results into a bitmask (a datastructure that uses one bit per page and where the first page corresponds to the least significant bit).
+You will receive full credit for this part of the lab if the
pgaccess
test case passes when runningpgtbltest
.这个信息没有放在题干提示,是在考察信息检索能力吗(
-栈的结构与栈帧的理解
-
这是来自hint的栈结构。整个栈存储在一页中,由高地址向低地址增长。栈帧代表了一次函数调用,其中会存储如函数名、函数参数、局部变量等等信息。有几次函数调用就有几个栈帧,栈由栈帧组成。
-s0中存储的栈帧指针fp指向的是栈帧的最高地址,如图fp所示。
+感想
实验内容:
+实现
+void pgaccess(uint64 sva,int pgnum,int* bitmask);
,一个系统调用。在这里面,我们要做的是,访问从sva
到sva+pgnum*PGSIZE
这一范围内的虚拟地址对应的PTE,然后查看PTE的标记项是否有PTE_A
。有的话则在bitmask对应位标记为1.应该注意的点:
+1.需要进行内核态到用户态的参数传递 2.需要进行系统调用的必要步骤 3.PTE_A需要自己定义
+以上是初见。做完了发现,确实就是那么简单,我主要时间花费在下的实验版本不对,折腾来折腾去了可能有一个小时,最后还是选择了直接把测试函数搬过来手工调用。已经换到正确的年份版本了【泪目】
+有一点我忽视了,看了提示才知道:
--我理解错了栈帧的定义,都怪我基础不大牢固也不认真思考【悲】我一开始以为stack frame指的是一个栈,也即一页空间【我知道栈帧这个中文名词,但遇到英语就短路了】。老师画的这个图也被我理解为多个栈,也即多页拼在一起,要打印的Return Address处于页的最顶部。我就在这个思路上一去不复返了,压根没有意识到一个进程只有一个栈【大悲】然后顺带脑补把r_fp()也曲解了,以为它的意思是读取当前栈【非常自然地认为有很多个栈←】的下一个栈的最低地址【因为栈换掉了,所以s0也会变成父亲的栈的地址】。于是就写出了这样的代码:
-- -
void
backtrace(){
printf("backtrace:\n");
uint64 kstack = PGROUNDUP((uint64)(myproc()->kstack)+1);
uint64 nstack = 0;
while((nstack=(uint64)r_fp())!=0){
printf("%p\n",*((uint64*)(kstack-8)));
kstack = nstack;
}
}结果最后死循环了。去看了别人的代码发现他们写的结构就跟我完全不一样。琢磨着画着图,最后找了stack frame的定义,才恍然大悟(
-思路形成
我们只需遍历栈中所有栈帧,打印每个栈帧的Return Address部分就行。通过r_fp()获取第一个栈帧的位置,其他栈帧的位置由Prev.Frame获取。循环的界限是PGROUNDUP(r_fp()),因为栈只有一页的空间。
-代码
- -
void
backtrace(){
printf("backtrace:\n");
uint64 stack = r_fp();
uint64 nstack = 0;
uint64 top = PGROUNDUP(stack);
while(stack!=top){
nstack=*((uint64*)(stack-16));
printf("%p\n",*((uint64*)(stack-8)));
stack = nstack;
}
}Alarm
--In this exercise you’ll add a feature to xv6 that periodically alerts a process as it uses CPU time. This might be useful for compute-bound processes that want to limit how much CPU time they chew up, or for processes that want to compute but also want to take some periodic action.
-More generally, you’ll be implementing a primitive form of user-level interrupt/fault handlers; you could use something similar to handle page faults in the application, for example.
-You should add a new
+sigalarm(interval, handler)
system call. If an application callssigalarm(n, fn)
, then after everyn
“ticks” of CPU time that the program consumes, the kernel should cause application functionfn
to be called. Whenfn
returns, the application should resume where it left off.Be sure to clear
PTE_A
after checking if it is set. Otherwise, it won’t be possible to determine if the page was accessed since the last timepgaccess()
was called (i.e., the bit will be set forever).感觉从alarm中可以窥见信号的实现思路:
--
而alarm的机理感觉也有点类似。用户先通过sigalarm注册定时函数,内核在时钟中断的时候对该标记位进行检查,然后去do_signal回到用户态执行用户的signal handler,再通过sigreturn回到用户模式。sigalarm相当于一个信号注册函数,sigreturn也就是上图的sigreturn。
-分析
初见思路
思路:sigalarm需要在用户程序在用户态运行的情况下,监测到用户程序已经运行了n个时间片,然后发出中断请求。我们会新设置一个中断类型alarm。kerneltrap接收到sigalarm的中断请求,检测到中断类型为alarm,就会在处理的时候调用fn。fn调用完就自然而然利用中断恢复到原来的现场了。所以要做的可以分为两部分。但问题是,如何让sigalarm在用户程序运行的同时监测n个时间片呢?难道得fork一个新的进程吗?然后父进程返回,子进程执行类似sleep里面那样的监测,直到时间片到了,就发送一个中断请求,让父进程停止,执行完fn回来之后就exit。
-正确思路
可以看到,初见思路很多地方跟最后是不一样的。其中错得最离谱的,也是比较隐坑很容易因为想不明白就寄了的,是handler是个用户态的函数(。你不可能在内核态中调用fn,然后fn执行完后再自然而然地通过中断机制返回,因为你想要执行fn就必须进入用户态。这一点是需要一开始明确的。
-明确了这一点后,让人更加不知道该怎么办了。那就一步步跟着指导书的脚步来思考吧。
-part 1
首先,明确你需要实现什么。你需要实现两个系统调用,一个是sigalarm,一个是sigreturn。结合提示,可知实验设计者给我们的思路是,通过sigalarm设置定时函数,通过sigalarm(0,0)取消定时函数。每次时钟中断检测当前定时时间是否达到,若已达到,则跳到定时函数执行。定时函数执行完后,需要借助sigreturn,才能正确返回时钟中断前的程序点。
-part 2
这又可拆解为几个要点:
--
-- 如何实现“定时”?
-- 时钟中断在内核态的usertrap被检测。怎么从usertrap出来跳到定时函数而非原程序执行点?
-- 执行完定时函数后,怎么样才能回到原程序执行点?
-part 3
一个个来说,首先是如何实现定时。这个很简单。参照sys_sleep的代码:
-+
uint64
sys_sleep(void)
{
int n;
uint ticks0;
backtrace();
if(argint(0, &n) < 0)
return -1;
acquire(&tickslock);
ticks0 = ticks;
while(ticks - ticks0 < n){
if(myproc()->killed){
release(&tickslock);
return -1;
}
sleep(&ticks, &tickslock);
}
release(&tickslock);
return 0;
}也就是说每次检查到一个,就需要手动清除掉PTE_A标记。
+还有一点以前一直没注意到的,头文件的引用需要注意次序。比如说要是把
+spinlock.h
放在proc.h
后面,就会寄得很彻底。代码
那些系统调用的登记步骤就先省略了。
+-
// kernel/sysproc.c
uint64
sys_pgaccess(void)
{
uint64 sva;
int pgnum;
uint64 bitmask;
if(argaddr(0,&sva) < 0 || argint(1, &pgnum) < 0 || argaddr(2, &bitmask) < 0)
return -1;
return pgaccess((void*)sva,pgnum,(void*)bitmask);
}可知,我们可以用ticks表示当前系统滴答数。这样,我们就可以在proc域里维护一个变量lasttick,记录上一次执行handler时的滴答数。每次在时钟中断时检测,所以需要写在
-kernel/trap.c
中的usertrap
中。part 4
然后,是怎么从usertrap出来跳到指定程序结束点。在书中,我们知道,sepc寄存器保存了中断前原程序的下一个执行点,sepc的备份存储在了proc域中的栈帧中。当中断返回时(在usertrapret中),会从栈帧中的epc字段读取sepc的备份赋值给sepc,再由sret帮助我们跳转到原程序点。因而,如果想要改变跳转点,我们只需要修改
-p->trapframe->epc
就行。part 5
最后,是如何从periodic回到原程序执行点。
--
这是每次进行时钟中断时的栈情况和执行代码链:t1->trampoline->usertrap->handler。
-再然后,handler调用了sigreturn,用户栈中就会产生sigreturn的栈帧:
--
此时,如果sigreturn执行完,就会在这样的情况下执行handler的ret指令:
--
ret指令会把栈帧弄走,也就是说会直接回到某个错误的地方去。这显然不大合适。所以,我们要做的,就是在sigreturn之后,不执行handler的ret指令,也不执行sigreturn的ret指令,而是直接恢复到时钟中断前的上下文。时钟中断前的上下文,会因在handler中调用sigreturn系统调用,而被覆盖,因而,我们就需要记录时钟中断前的上下文,也即在proc域中保存trapframe的一份拷贝savedtrap,每次时钟中断都更新一次savedtrap,然后在sigreturn调用的时候将proc原本的trapframe替换为savedtrap即可。这样一来,就完成了这道题。
-感想
这题目确实最终代码看起来完全不难,但是非常地拷打。。。。我前前后后修修补补差不多一共花了五个小时之久。
-计时怎么计,以及使用trapframe->epc来跳转这两点还是很容易想到的【话虽如此,其实也很曲折】。主要难点还是在怎么恢复现场。怎么说呢,我花了这么久做实验,但是实际笔记却写不出个鬼来,足以看出其复杂程度。
-我主要还是思维固化了点,一直在想,怎么确保它正确返回现场。我一开始以为proc域保存一个寄存器状态,且只用在一开始设置定时函数也即sigalarm的时候保存一次就行了,并且认为其是epc。然而实际操作后发现usertrap崩了,并且epc中存储的并不是程序被时钟中断的地方,而是各种神奇的地方,具体我也忘了,反正不能行。我印象最深刻的是有一次停在了usys.S中的sigreturn的最后一个ret处。我就在想,也许是栈出了问题。于是我就想着直接在sigreturn的时候把epc指向栈帧中的return address,直接回到原执行段。我百度了一下,确实有这么个寄存器ra,存储着return address。于是我就把proc域的状态换成了ra,依然仅保存一次,最后发现还是不行,程序在test0之后就异常终止了,main也回不去,十分古怪,十分匪夷所思。我实在没忍住,百度了一下大家怎么做的,发现大家压根没有我这样的二选一的烦恼,是直接保存整个栈帧。而且也不是仅保存一次,而是每次时钟中断触发都保存一次。我觉得十分奇怪震惊,但此时已是差不多晚饭时间,我就先去吃了个饭()
-回来之后,我细细画了图【向正确思路part5中的那样】,发现我原来那个只保存两者之一,且都只保存一次的方式,确实完全不能行。但是,我发现两个一起保存,并且每次时钟中断保存的方法,似乎能行,而且,比保存一整个栈帧要聪明得多。于是我就去试了,发现还是不行。我再细想了一遍,发现,如果想回去原程序的现场,除了ra和epc,还有一个很重要的东西需要保存,那就是——用户栈指针sp!
-也就是说,只需保存ra、epc、sp,就可以保证回到正确的时钟中断前的位置:
--
此为handler中sigreturn执行完要返回时的状态。
-当处在handler中时,sp的值为sigreturn处的栈帧。执行系统调用时,proc域中的上下文被覆盖,也即时钟中断前的上下文被覆盖。如果此时不对栈帧中的sp进行恢复,仅恢复ra和epc,在从sigreturn返回到epc对应处也即t1,t1执行ret想回到main的时候,就会回不去,而是回到了sigreturn要回的位置,也即handler的位置,然后不知不觉就寄了。所以,就需要防止sp被覆盖。因而,再保存一个状态sp,就可以保障回到正确的地方了。测试出来,kernel确实不再会panic了。
-但是由于运行时很多除这三个以外的寄存器都被改过了,回是回得去,接下来干的活就不一定对了。因此为了保险以及通用性以及便利性来看,还是像别人那样直接保存栈帧比较ok。
-还有一件事,就是上述错误中经常会出现的一个输出结果:
-+
usertrap:unexpected cause scause = 0x0c-
// kernel/pgaccess.c
int
pgaccess(void* sva,int pgnum,void* bitmask){
if(pgnum > 32){
printf("pgaccess: range too big.\n");
exit(1);
}
int kmask = 0;
struct proc* p = myproc();
for(int i=0;i<pgnum;i++){
pte_t* pte = walk(p->pagetable,(uint64)sva+i*PGSIZE,0);
// 映射不存在,或者没有被访问过
if(!pte || !(*pte & PTE_A)){
continue;
}
kmask = (kmask | (1<<i));
*pte = (*pte & (~PTE_A));
}
copyout(p->pagetable,(uint64)bitmask,(char*)(&kmask),sizeof(int));
return 1;
}我留意了一下是什么意思。网上搜索得,scause=12,说明这是一个instruction page fault,而这个缺页错误说明了什么?:
--
这样,一切都明朗了。出现了scause=0x0c的意思就是说pc里的值不恰当,也就是说上面错误的方法都会跳转到错误的地方去。
-Lab:xv6 lazy page allocation
-参考:https://blog.csdn.net/m0_53157173/article/details/131349366
+A kernel page table per process
+The goal of this section and the next is to allow the kernel to directly dereference user pointers.
--来自书本:
-Another widely-used feature is called lazy allocation, which has two parts:
--
-- First, when an application calls sbrk, the kernel grows the address space, but marks the new addresses as not valid in the page table.
-- Second, on a page fault on one of those new addresses, the kernel allocates physical memory and maps it into the page table.
-The kernel allocates memory only when the application actually uses it.
-Eliminate allocation from sbrk()
--Your first task is to delete page allocation from the sbrk(n).
-The sbrk(n) system call grows the process’s memory size by n bytes, and then returns the start of the newly allocated region (i.e., the old size). Your new sbrk(n) should just increment the process’s size (myproc()->sz) by n and return the old size. It should not allocate memory – so you should delete the call to growproc() (but you still need to increase the process’s size!).
+Your first job is to modify the kernel so that every process uses its own copy of the kernel page table when executing in the kernel.
+Modify
struct proc
to maintain a kernel page table for each process, and modify the scheduler to switch kernel page tables when switching processes. For this step, each per-process kernel page table should be identical to the existing global kernel page table. You pass this part of the lab ifusertests
runs correctly.+
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
int sz = myproc()->sz;
addr = sz;
myproc()->sz = sz + n;
//if(growproc(n) < 0)
// return -1;
return addr;
}感想
这个其实平心而论不难,思路很简单。写着不难是不难,但想明白花费了我很多时间。
+它这个要求我们修改kernel,使得每个进程都有一份自己的kernel page。至于要改什么,围绕着proc.c中,参照pagetable的生命周期摁改就行。还有一个地方它也提示了,就是要在swtch之前更换一下satp的值。
+接下来,我说说我思考的几个点以及犯错的地方。
+为什么要这么干
看完题目,我的第一印象是,这么干有啥用。。。因为我觉得以前那个所有进程共用内核页表确实很好了,没有必要每个进程配一个后来才发现,这个跟下面那个是连在一起的,目的是
+allow the kernel to directly dereference user pointers.
。所以,我们下面会把用户的pgtbl和这里dump出来的kpgtbl合在一起。具体来说:
+通常,进行地址翻译的时候,计算机硬件(即内存管理单元MMU)都会自动的查找对应的映射进行翻译(需要设置
+satp
寄存器,将需要使用的页表的地址交给该寄存器)。然而,在xv6内核需要翻译用户的虚拟地址时,因为内核页表不含对应的映射,计算机硬件不能自动帮助完成这件事。因此,我们需要先找到用户程序的页表,仿照硬件翻译的流程,一步一步的找到对应的物理地址,再对其进行访问。【
+walkaddr
】这也就会导致copyin之类需要涉及内核和用户态交互的函数效率低下。为了解决这个问题,我们尝试将用户页表也囊括进内核页表映射来。但是,如果将所有进程的用户页表都合并到同一个内核全局页表是不现实的。因而,我们决定换一个角度,让每个进程都仅有一张内核态和用户态共用的页表,每次切换进程时切换页表,这样就构造出了个全局的假象。
+这两次实验就是为了实现该任务。在本次实验中,我们首先先实现内核页表的分离。
+关于myproc()
在allocproc中初始化的时候,我一开始是这么写的:
+-
// in proc.c allocproc()
perproc_kvminit();Lazy allocation
-Modify the code in trap.c to respond to a page fault from user space by mapping a newly-allocated page of physical memory at the faulting address, and then returning back to user space to let the process continue executing.
-You should add your code just before the
+printf
call that produced the “usertrap(): …” message. Modify whatever other xv6 kernel code you need to in order to getecho hi
to work.+ +
// in vm.c
pagetable_t
perproc_kvminit()
{
struct proc* p = myproc();
p->kpgtbl = (pagetable_t) kalloc();
memset(p->kpgtbl, 0, PGSIZE);
// uart registers
pkvmmap(p->kpgtbl,UART0, UART0, PGSIZE, PTE_R | PTE_W);
// ...
return pt;
}这样会死得很惨,爆出如下panic:
++
通过hints的调试贴士
++-A missing page table mapping will likely cause the kernel to encounter a page fault. It will print an error that includes
sepc=0x00000000XXXXXXXX
. You can find out where the fault occurred by searching forXXXXXXXX
inkernel/kernel.asm
.感想
思路
首先,要知道缺页中断的scause为13或15.【论我怎么知道的:被以前的实验逼出来的hhh】然后,要写在if条件的第二个分支。在该分支内,我们需要先获取出问题的地方的虚拟地址的值,然后申请新的一页,再map到当前页表中。
-一个难以察觉的错误
描述
思路是很简单的,就是有小细节需要格外注意。
-trap.c在
-mappages
时,一定不能直接传入va,必须传入PGROUNDDOWN(va)
。如果直接传入va,会爆出如下错误:-
但是,查看
-mappages
的代码:+
int
mappages(pagetable_t pagetable, uint64 va, uint64 size, uint64 pa, int perm)
{
uint64 a, last;
pte_t *pte;
a = PGROUNDDOWN(va);
last = PGROUNDDOWN(va + size - 1);
for(;;){
if((pte = walk(pagetable, a, 1)) == 0)
return -1;
if(*pte & PTE_V)
panic("remap");
*pte = PA2PTE(pa) | perm | PTE_V;
if(a == last)
break;
a += PGSIZE;
pa += PGSIZE;
}
return 0;
}我发现程序在这里绷掉了:
+-
p->kpgtbl = (pagetable_t) kalloc();我们可以看到,它在里面已经对va进行了处理了,使它变成了page-align的a变量。那么为什么,我们还要在外面再对va处理一次呢?
-其实问题不是出在
-mappages
中的a变量上,而是出现在mappages
中的last变量上。比如,令va=PGSIZE+200,则a=PGSIZE,last=2*PGSIZE。这样一来,在下面的循环中,除了添加了刚刚申请的那页的映射以外,我们还多添加了新的一页,其物理地址为mem+PGSIZE。这十分地危险!假设你要申请的va为proc->size的最后一页,那么,经过本次缺页中断之后,你事实上申请了两页,两页的地址分别为va和va+PGSIZE。而va+PGSIZE大于proc->size。也就是说,地址溢出了!
-这会导致页表释放的时候出问题。以下是页表释放的路径。
-+
// in proc.c
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, sz);
}
// in vm.c
void
uvmfree(pagetable_t pagetable, uint64 sz)
{
if(sz > 0)
uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 1);
freewalk(pagetable);
}
// in vm.c
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
if((va % PGSIZE) != 0)
panic("uvmunmap: not aligned");
for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
if((pte = walk(pagetable, a, 0)) == 0){
printf("uvmunmap walk: %p\n",a);
continue;
}
if((*pte & PTE_V) == 0){
printf("uvmunmap: %p\n",a);
continue;
}
printf("uvmunmap OK: %p\n",a);
if(PTE_FLAGS(*pte) == PTE_V)
panic("uvmunmap: not a leaf");
if(do_free){
uint64 pa = PTE2PA(*pte);
kfree((void*)pa);
}
*pte = 0;
}
}而且显而易见,是系统启动时崩的。
+经过了漫长的思考,我震惊地发现了它为什么崩了()
+首先,这段代码语法上是没有问题的。它固然犯了发布未初始化完成的对象这样的并发错误【我有罪】,也破坏了proc的封装性【proc中的很多私有属性本来应该作用域仅在proc.c中的。此处为了能让vm.c访问到proc中的属性,不得不给vm.c添上了proc.h的头文件】,但是它并不是语法错误,还是能用的。我做了这样的测试样例证明它没有问题:
+-
typedef int pagetable_t;
struct proc{
pagetable_t kpgtbl;
};
struct proc processes[MAX];
struct proc* myproc(){
return &processes[0];
};
void kvminit(){
myproc()->kpgtbl = 1;
}
int main(){
struct proc* p = &processes[0];
kvminit();
printf("%d",p->kpgtbl);
return 0;
}freewalk要求在uvmunmap中已经释放完所有的叶子结点。而由于uvmunmap中释放结点的va是从0递增到proc->size的,也因而,前面的那个大于proc->size的那页虽然还在页表中存在,但是不会被uvmunmap释放!这也就导致,接下来调用freewalk的时候,会发现该页的叶子结点仍然存在,从而导致
-freewalk: leaf
。可以结合uvmunmap和trap.c中的调试语句看下图的过程,可以看到非常清晰明了,0x14000这一页并没有在uvmunmap中释放!
-trap.c中的调试语句:
-+
printf("trap: %p,+PGSIZE = %p\n",PGROUNDDOWN(va),PGROUNDDOWN(va)+PGSIZE);
if(mappages(p->pagetable,va, PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
kfree(mem);
}我一路顺着os启动的路径找,也想不出来这能有什么错,因而非常迷茫。
+此时我灵光一闪,会不会是myproc()在os刚启动的时候是发挥不了作用的?于是我一路顺着myproc的代码看下去:
+-
struct proc*
myproc(void) {
push_off();
struct cpu *c = mycpu();
struct proc *p = c->proc;
pop_off();
return p;
}图:
--
debug过程
看到
-freewalk: leaf
这一错误,很容易联想到跟页表的释放有关。并且加上PGGROUNDDOWN就没问题,不加上才有问题,也很容易联想到跟mappages中多申请的那一页有关。但是具体是什么关系,这一点想要想到对我来说还是非常曲折的。我一开始,以为是因为多申请的那一页(下面简称为B页好了)很有可能是其他进程在使用的,然后其他进程在echo进程释放页表前释放了页表,从而导致B页已经free了,这样一来
-uvmunmap
说不定就能监测到对应物理页已经free,然后爆出panic。我一开始认为uvmunmap
的这句话是用来监测物理页是否free的:+
if((*pte & PTE_V) == 0){
continue;
//panic("uvmunmap: not mapped");
}那么,mycpu()获得的cpu的proc是怎么得到的呢?
+我搜寻了一下os启动代码,发现了cpu的proc得到的路径。
+-
void
main()
{
if(cpuid() == 0){
consoleinit();
printfinit();
printf("\n");
printf("xv6 kernel is booting\n");
printf("\n");
//...很多很多init
userinit(); // first user process
__sync_synchronize();
started = 1;
} else {
// ...
}
//调度执行第一个进程
scheduler();
}然后顺理成章地,这边条件==0成立,然后continue,然后直到freewalk才发现该pte未释放。
-但是,我仔细过脑子想了想,发现,就算物理页已经free了,但是,*pte依然存在,PTE_V也依然为1,这个条件是不成立的。也就是说,B页不会continue,而是会继续下面的正常释放的流程。也就是说,B页是可以正常释放的,我们的“B页已经free导致uvmunmap释放失败”的推论是错误的。
-但究竟是为什么呢?肯定跟B页有关系,但是又不是这种关系,这让我十分地苦恼且烦躁,于是我就去打了会儿游戏。边玩的时候突然注意到一件非常可疑的事情。
--
这是发生错误时退出的截图。有一个点引起了我的注意,就是echo hi并没有打印hi在console上。也就是说,这个panic是在echo执行前产生的!那么这个执行前是在哪呢?答案就是在exec中!
-+
// in exec.c
// Commit to the user image.
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
p->trapframe->epc = elf.entry; // initial program counter = main
p->trapframe->sp = sp; // initial stack pointer
// 这里!!!!!
proc_freepagetable(oldpagetable, oldsz);创建完进程后,就进入scheduler进行进程的调度:
+-
void
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();
// ...
int found = 0;
for(p = proc; p < &proc[NPROC]; p++) {
// ...
//在这里!!!!
c->proc = p;
swtch(&c->context, &p->context);
c->proc = 0;
// ...可以看到,这里调用了
-proc_freetable
,从而跟freewalk有了联系。但是,如果我们还坚持是因为B页错的,就需要找到一个可能会产生B页的地方,也就是验证shell准备执行echo命令,fork出一个子进程之后,又在exec free页表前,已经调用过sbrk函数,并且已经触发过缺页中断。这个验证其实很简单,只需要找sbrk在哪被调用过,哪边使用过heap内存【也即哪边涉及了指针赋值】就行了。
-通过全局搜索,可知sbrk在
-user/umalloc.c
下的malloc()
被使用过,而在user/sh.c
中,fork子进程之后:+
if(fork1() == 0)
runcmd(parsecmd(buf));因而,c->proc是在创建进程的第一次调度后初始化的,也即,myproc只有在执行第一次scheduler之后才可以调用。而!!!
+当执行调度前的userinit时:
+-
void
userinit(void)
{
struct proc *p;
p = allocproc();
initproc = p;这其中的
-parsecmd()
中malloc被使用过,并且发生了指针赋值!!也就是说,“是因为B页错的”这个结论是对的。虽然这一段debug没有改变我们要证明“是B页在释放内存中出错的”的这个目的,但是确实带给了我很多这种执行时申请内存的知识,并且也让我突然想起了可以用printf debug。于是,我就去做了上面那个在trap.c中和uvmunmap中printf的调试语句,最终成功发现了结论。
-实在是太艰苦了()这告诉我们以后千万千万要注意,是否需要用到PGGROUNDDOWN。
-一个漏掉未考虑的细节
摘自https://blog.csdn.net/m0_53157173/article/details/131349366
--
-
不过我以前好像是有考虑到这个的,但是我是这么做的:
--
也就是相当于把它在addr parse的那段代码移进了walkaddr中。但是这样是不行的,查找可知argaddr的应用范围可比walkaddr广得多……
-而为什么我下面的COW也是修改了walkaddr,而非修改argaddr,就可以达到同样的效果呢?这是因为cow只需对在内核中写用户页这种情况进行特殊处理,而这只有一个情况,也即只在copyout中发生。因而,我们只需修改walkaddr,就可以完全防范该情况了。
-Lazytests and Usertests
--We’ve supplied you with
-lazytests
, an xv6 user program that tests some specific situations that may stress your lazy memory allocator. Modify your kernel code so that all of bothlazytests
andusertests
pass.感想
一个绷不住的错误
其实很简单,按照提示一步步做就行了。为什么我做得那么久那么崩溃呢?知道原因后我都笑嘻了。
-在第一步修改
-sys_sbrk()
的时候,我一下子没多想,使用了一句int sz = myproc()->sz
,其实本来应该使用uint64的,使用int会溢出。这个伏笔就一直隐含到这里,然后大坑了我一笔。一开始是发现
-lazytests
的第二个,也就是oom过不去。我想了很久,也去网上找了别人的代码一步步对比下来看了,没有发现特别大的问题。于是我就在walk和sys_sbrk分别留下了调试信息:+
// in walk()
if(va >= MAXVA){
printf("walk:va=%p,p->sz=%p,MAXVA=%p,pgva=%p\n",va,myproc()->sz,MAXVA,PGROUNDDOWN(va));
panic("walk");
}它进行了allocproc。我们亲爱的allocproc接下来就会调用perproc_kvminit,然后perproc_kvminit中调用myproc。此时尚未进行初次调度,因而c->proc未初始化,myproc返回的是0,也即null。这样一来,
+myproc()->kpgtbl
就发生了空指针异常,也即scause = 15——写入页错误。因而,对于myproc()的调用需要慎之又慎。
+系统调用
系统调用时,是如何知道要用的是p中的内核页表而非global内核页表呢?
+依然还是从os的启动说起。
+在main.c中,kvminithart开启了页表,此时的页表为全局的内核页表:
+-
// Switch h/w page table register to the kernel's page table,
// and enable paging.
void
kvminithart()
{
w_satp(MAKE_SATP(kernel_pagetable));
sfence_vma();
}+
// in sys_sbrk()
if(n >= 0){
uint64 tmp = n + sz;
if(tmp > MAXVA || n + sz < n) return -1;
myproc()->sz = tmp;
//printf("haha sb!sz=%p,n=%p\n",myproc()->sz,n);
}当userinit被调度时,全局的内核页表被换成了proc中的内核页表:
+-
// in proc.c scheduler()
p->state = RUNNING;
w_satp(MAKE_SATP(p->kpgtbl));
sfence_vma();
c->proc = p;
swtch(&c->context, &p->context);然后发现了这样的输出:
--
可以看到,最后一次sz发生了数值溢出。
-但是,此时我并没有悔改。我反而认为,“原本代码就是这么写的”。也就是说,我认为
-int sz
是它原本内核代码给的。。。。。。在这样的情况下,我选择加上这样的条件判断:+
if(tmp > MAXVA || ((tmp >> 31)& 1) == 1) return -1;但是这样还没有结束。因为我们除了得更换目前的页表,还得更换trapframe中的内核页表相关的东西:
+-
struct trapframe {
/* 0 */ uint64 kernel_satp; // kernel page table
/* 8 */ uint64 kernel_sp; // top of process's kernel stack
}之后确实没有溢出了,但是test fail了。此时我想,为什么非要用int而不用uint64呢?一阵令人不寒而栗的预感袭来,我连忙去看了
-proc.h
里的sz的定义,发现,sz原本就应该是uint64类型的,是我错辣【悲】只能说起到一种很好的教训。主要是这种问题实在没有想过自己会犯
-代码
---
-- Handle the parent-to-child memory copy in fork() correctly.
-+
// in vm.c uvmcopy()
if((pte = walk(old, i, 0)) == 0)
continue;
//panic("uvmcopy: pte should exist");
if((*pte & PTE_V) == 0)
continue;
//panic("uvmcopy: page not present");为啥还要更换trapframe中的呢?因为以后系统调用的时候,uservec是从这里读取值来作为内核栈和内核页表的来源的:
+-
# in uservec
# restore kernel stack pointer from p->trapframe->kernel_sp
# 完成了内核栈的切换
ld sp, 8(a0)
# 完成了页表的切换
# restore kernel page table from p->trapframe->kernel_satp
ld t1, 0(a0)
csrw satp, t1
sfence.vma zero, zero---
-- Handle negative sbrk() arguments.
-+
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
uint64 sz = myproc()->sz;
addr = sz;
if(n >= 0){
uint64 tmp = n + sz;
if(tmp > MAXVA || n + sz < n) return -1;
myproc()->sz = tmp;
//printf("haha sb!sz=%p,n=%p\n",myproc()->sz,n);
} else{
if(n + sz > 0)
myproc()->sz = uvmdealloc(myproc()->pagetable, sz, sz + n);
else
return -1;
}
//if(growproc(n) < 0)
// return -1;
return addr;
}所以,为了以后系统调用能顺利自发进行,我们需要把栈帧也一起换掉。怎么换呢?我们是否还要在一些地方人工把trapframe的值设置为我们自己的内核栈内核页表?答案是,不用!这些会由其他代码自动完成。
+前面说到userinit的进程p被调度,satp换成了我们自己的内核页表。那么,在之后的内核态,satp都将保持我们自己的内核页表。当要返回用户态时,会执行如下代码:
+-
// in usertrapret
// 重置trapframe
p->trapframe->kernel_satp = r_satp(); // kernel page table
p->trapframe->kernel_sp = p->kstack + PGSIZE; // process's kernel stack---
-- -
Kill a process if it page-faults on a virtual memory address higher than any allocated with sbrk().
-- -
Handle out-of-memory correctly: if kalloc() fails in the page fault handler, kill the current process.
-- -
Handle faults on the invalid page below the user stack.
-+
else if(scause == 13 || scause == 15){
// 缺页中断
uint64 va = r_stval();
if(va >= p->sz || va < PGROUNDUP(p->trapframe->sp) ||PGROUNDDOWN(va) >= MAXVA){
//printf("va=%p stack=%p\n",va,PGROUNDUP(r_sp()));
p->killed = 1;
} else{
char* mem = kalloc();
if(mem != 0){
memset(mem, 0, PGSIZE);
//printf("trap: %d %p\n",p->pid,PGROUNDDOWN(va));
if(mappages(p->pagetable,PGROUNDDOWN(va), PGSIZE, (uint64)mem, PTE_W|PTE_X|PTE_R|PTE_U) != 0){
kfree(mem);
p->killed = 1;
}
} else {
p->killed = 1;
}
}
}satp内的值为我们自己的内核页表,而非全局页表。因而这样栈帧中的页表就会被自然而然地写入为进程的内核页表。之后返回用户态,以及之后之后的各种中断,就都会一直使用自己的内核页表了。【试了一下,这里如果改成非即时从satp读,而是默认的kernel_pagetable的话,会一直死循环】
+不得不说,真是设计精妙啊!!!不过我觉得,要是这里写成kernel_pagetable,然后让我们自己改的话将是薄纱(。当然它应该也不会这么做,因为,kernel_pagetable事实上是不对外发布的。它这里这么写热读,最直接的原因还是因为读不到kernel_pagetable。这算是无心插柳柳成荫吗233
+释放页表但不释放物理内存
其实答案就在它给的
+proc_freepagetable
里。-
// Free a process's page table, and free the
// physical memory it refers to.
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, TRAPFRAME, 1, 0);
uvmfree(pagetable, sz);
}---
-- Handle the case in which a process passes a valid address from sbrk() to a system call such as read or write, but the memory for that address has not yet been allocated.
-我认为这里要是引起一个缺页中断可能会更酷,可能可以像lazytests里面这么做:
-+
char *i, *prev_end, *new_end;
prev_end = sbrk(REGION_SZ);
new_end = prev_end + REGION_SZ;
// 这里触发了多次缺页中断
for (i = prev_end + PGSIZE; i < new_end; i += PGSIZE * PGSIZE)
*(char **)i = i;+
uvmfree
遍历页表,对每个存在的页表项,都试图找到其物理内存,并且释放物理内存和表项。如果页表项存在,但页表项对应的物理内存不存在,就会抛出freewalk leaf
的异常。+
uvmunmap
会释放掉参数给的va的页表项,最后一个参数表示释放or不释放。在这里,使用这两个的组合技,就可以达到不释放
+TRAMPOLINE
和TRAPFRAME
的物理内存,又不会让uvmfree出错的效果。代码
初始化
初始化kpgtbl。由于现在内核栈存在各自的内核页表而非global内核页表中,所以在procinit中的对内核栈的初始化也得放在这:
++ +
// in proc.c allocproc()
// An empty user page table.
p->pagetable = proc_pagetable(p);
if(p->pagetable == 0){
freeproc(p);
release(&p->lock);
return 0;
}
p->kpgtbl = perproc_kvminit();
char *pa = kalloc();
if(pa == 0)
panic("kalloc");
uint64 va = KSTACK((int) (p - proc));
pkvmmap(p->kpgtbl,va, (uint64)pa, PGSIZE, PTE_R | PTE_W);
p->kstack = va;-
// in vm.c
pagetable_t
perproc_kvminit()
{
pagetable_t pt = (pagetable_t) kalloc();
memset(pt, 0, PGSIZE);
// uart registers
pkvmmap(pt,UART0, UART0, PGSIZE, PTE_R | PTE_W);
// virtio mmio disk interface
pkvmmap(pt,VIRTIO0, VIRTIO0, PGSIZE, PTE_R | PTE_W);
// CLINT
pkvmmap(pt,CLINT, CLINT, 0x10000, PTE_R | PTE_W);
// PLIC
pkvmmap(pt,PLIC, PLIC, 0x400000, PTE_R | PTE_W);
// map kernel text executable and read-only.
pkvmmap(pt,KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
// map kernel data and the physical RAM we'll make use of.
pkvmmap(pt,(uint64)etext, (uint64)etext, PHYSTOP-(uint64)etext, PTE_R | PTE_W);
// map the trampoline for trap entry/exit to
// the highest virtual address in the kernel.
pkvmmap(pt,TRAMPOLINE, (uint64)trampoline, PGSIZE, PTE_R | PTE_X);
return pt;
}之后有机会再试试233
-【试了一下,发现是可以的。在COW fork的
-感想—一些错误和思考—在内核态中引发并处理缺页中断
这部分内容中详细说明了具体要怎么做。】Lab: Copy-on-Write Fork for xv6
--Parent and child can safely share phyical memory using copy-on-write fork, driven by page faults.
-RISC-V has three different kinds of page fault: load page faults (when a load instruction cannot translate its virtual address), store page faults (when a store instruction cannot translate its virtual address), and instruction page faults (when the address for an instruction doesn’t translate).
-The basic plan in COW fork is for the parent and child to initially share all physical pages, but to map them read-only. Thus, when the child or parent executes a store instruction, the RISC-V CPU raises a page-fault exception. In response to this exception, the kernel makes a copy of the page that contains the faulted address. It maps one copy read/write in the child’s address space and the other copy read/write in the parent’s address space. After updating the page tables, the kernel resumes the faulting process at the instruction that caused the fault.
-PS【这个很重要】: COW fork() makes freeing of the physical pages that implement user memory a little trickier. A given physical page may be referred to by multiple processes’ page tables, and should be freed only when the last reference disappears.
-感想
思路
思路还是很直观的。
-我们只需要在fork的时候,标记父子进程的所有PTE都为read-only,然后之后就会遇到不同scause的缺页中断,针对特定的scause,新建物理页面,拷贝物理页面,然后重新设置映射即可。而对于其提出的需要标记某页是否能够释放,则需要统计每页的ref数,当ref==1的时候才可以释放。
-分析
总分析
其实可以把任务简单拆分为三部分。第一部分是实现基本的cow fork的逻辑,第二部分是引用计数释放内存,第三部分是解决copyin/copyout时在内核态发生的缺页中断。我认为本实验的难点事实上在第二部分【悲】我可能有大于3/4的时间都花在第二部分上了吧。
-第一部分是实现cow fork的基本逻辑,也就是修改fork中对页表的拷贝以及在usertrap中添加对缺页中断的处理,这很直观,没什么好说的。
-第三部分要么跟上面的lazy allocation一样,在
-kernel/vm.c walkaddr()
中把缺页中断搬过去,要么向我在主要难点与错误—在内核态中引发并处理缺页中断
这一部分那样做。我们分析这一部分主要讲的是我认为最难的地方,也就是第二部分。其实第二部分的思路也很直观:创建一个数组,index为所有能用的内存的
-address/PGSIZE
,用来记录引用数;然后在每次增加引用时,对应元素++;每次减少引用时,对应元素–。虽然思路很简单很直观,但是实现起来非常地非常地非常地考验细节(我就非常不擅长这一点)。下面,我就先阐述一下第二部分的这个方法需要分割为哪几部分,其他我遇到的印象较深的bug和对一些地方的思考,都放在了下一部分,也即
-主要难点与错误
。引用数实现分析
--创建一个数组,index为所有能用的内存的
-address/PGSIZE
,用来记录引用数;然后在每次增加引用时,对应元素++;每次减少引用时,对应元素–。数组的大小和数据类型
由
-kernel/kalloc.c
中的kinit()
:+
void
kinit()
{
initlock(&kmem.lock, "kmem");
// freerange用来把参数地址范围内的物理页加入freelist中
// end是内核的结束地址
freerange(end, (void*)PHYSTOP);
}-
// in vm.c
void
pkvmmap(pagetable_t pgtbl,uint64 va, uint64 pa, uint64 sz, int perm)
{
// 当第一个进程开始时,mycpu->proc = null,所以这里不能调用myproc
if(mappages(pgtbl, va, sz, pa, perm) != 0)
panic("kvmmap");
}可知,事实上,我们整个程序,包括用户和内核,能用的内存空间为0~PHYSTOP。因而,我们事实上只需要建一个
-PHYSTOP/PGSIZE
这么大的数组就行。我算了一下大概是2^19次方。然后,我感觉这种小系统应该不会有过多的对某一页的重复引用,因而,为了节省空间,我将数据类型定为了char。最好还是别定成uchar,因为这东西要是0–的话会溢出变为255,很可怕。
-什么时候增减引用
--我认为这里是非常考验细节和头脑清晰度的,也就是我卡了很久最后也没弄出来的部分【悲】
-可以分为三种情况来讲。我们的引用计数必须完美适应这三种情况:
--
-- -
不经由页表,通过kalloc和kfree直接使用物理页
-这就要求我们在kalloc的时候置引用数为1,然后kfree的时候对引用数先-1,再判断是否归零。
-- -
经由页表,但与cow fork无关
-增加页表项:mappages->kalloc,因而满足要求1即可。
-删除页表项:uvmunmap。当do_free==1时,满足要求1即可。
-- -
经由页表,与cow fork有关
-copy父进程页表时:在cowcopy中,每增加一次子进程的映射,就需要增加一次引用数
-在用户态/内核态发生缺页中断:发生缺页中断后,对原来物理页的引用数需要-1【我就是漏了这一点……】
-删除页表项:uvmunmap。当do_free==0时,当对应页表项有COW标记,则减少引用数
-所以,我们需要在三个文件进行修改:
--
-- -
kalloc.c
-增加数组定义,在kalloc和kfree中增加引用数修改
-- -
vm.c
-在cowcopy和uvmunmap中增加引用数修改
-- -
trap.c
-在usertrap的缺页中断中增加引用计数修改
-并发安全
--这里我也没想到【悲】
-由于我们的pages数组会在多个文件、多个进程间使用,所以它必须在被锁保护的区域中被使用。
-主要难点与错误
scause=2
-
这个发生在我还没有实现第二部分的时候。搜索了一下,scause=2为
-Illegal instruction
,而且sepc的这个1004的值也非常诡异。这应该是因为fork子进程释放了指令段内存,导致主进程执行错误kernel无法启动
在kinit中
-+
void
kinit()
{
initlock(&kmem.lock, "kmem");
initlock(&pages_lock,"pages");
memset(pages, 0, (2<<19));
freerange(end, (void*)PHYSTOP);
}swtch时切换页表
-
// in proc.c scheduler()
p->state = RUNNING;
w_satp(MAKE_SATP(p->kpgtbl));
sfence_vma();
c->proc = p;
swtch(&c->context, &p->context);
//...
if(found == 0) {
// 没有进程运行时使用全局kernel_pagetable
kvminithart();
intr_on();
asm volatile("wfi");
}会通过freerange初始化freelist。在freerange中:
-+
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
kfree(p);
}
}修改kvmpa
-
uint64
kvmpa(uint64 va)
{
uint64 off = va % PGSIZE;
pte_t *pte;
uint64 pa;
pte = walk(myproc()->kpgtbl, va, 0);
if(pte == 0)
panic("kvmpa");
if((*pte & PTE_V) == 0)
panic("kvmpa");
pa = PTE2PA(*pte);
return pa+off;
}会对每一项进行一次kfree。因而,我们需要在kfree前先增加一次引用,要不然会寄。
-在缺页中断时减少对物理页的引用数
-
注意此处不能直接让pages[pa/PGSIZE]–,一定要借助kfree。当此进程为引用pa的最后一个进程的时候,如果仅减少引用数,就会造成内存泄漏。kfree可以既减少引用数,又在适当的时候对物理页释放,可谓一举两得。kfree的这个双重作用思想也在uvmunmap中体现了。
-在内核态中引发并处理缺页中断
--Modify copyout() to use the same scheme as page faults when it encounters a COW page.
-我们所做的第一第二部分仅仅是完成了对来自用户态的缺页中断的完美处理,还尚未处理来自内核态的缺页中断。因而,这个修改copyin和copyout的点实际上就是要我们处理内核态的缺页中断。
-这次实验跟上次的lazy allocation一样,都可以直接在walkaddr进行特殊处理,并且差不多要把usertrap的全部代码挪过来【具体见lazy allocation的代码】。不过,我想出了另一个流氓的方法(也就是说其实原理感觉是不大对233)。我选择直接在kernel引发一个访问用户页面的缺页中断,然后在kerneltrap中处理这个中断,就像usertrap一样。
-但由于在
-walkaddr
中发生的中断处于内核状态下,所以就进不了usertrap。我们应该在kerneltrap中再次添加和usertrap一样的中断处理。我们会像这样引发一个中断:+
if((*pte & PTE_COW) != 0){
if((*pte & PTE_W) == 0){
*(char*)va = 1;// 此处不能用pa哦释放
-
// in kernel.proc.c freeproc()
if(p->kpgtbl)
proc_freekpgtbl(p->kpgtbl,p->kstack);
p->kpgtbl = 0;然后在kerneltrap中这样处理:
-+
if(r_scause() == 15){
// 只要写入引起的缺页中断
uint64 va = r_stval();
pte_t *pte;
uint64 pa;
uint flags;
if((pte = walk(p->pagetable, va, 0)) == 0)
p->killed = 1;
else if((*pte & PTE_V) == 0)
p->killed = 1;
else {
sepc += 4;
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
char* mem;
if((mem = kalloc())!=0){
memmove(mem, (char*)pa, PGSIZE);
// 设置为新的物理页地址
*pte = PA2PTE(mem);
kfree((void*)pa);
// 设置新的flag,标记为可写
flags = (flags | PTE_W | PTE_COW);
*pte = ((*pte) | flags);
} else{
p->killed = 1;
}
}
} else if((which_dev = devintr()) == 0){-
extern char etext[]; // kernel.ld sets this to end of kernel code.
void
proc_freekpgtbl(pagetable_t pagetable,uint64 stack )
{
uvmunmap(pagetable, UART0, 1, 0);
uvmunmap(pagetable, VIRTIO0, 1, 0);
uvmunmap(pagetable, CLINT, 0x10000/(uint64)PGSIZE, 0);
uvmunmap(pagetable, PLIC, 0X400000/(uint64)PGSIZE, 0);
uvmunmap(pagetable, KERNBASE, (uint64)((uint64)etext-KERNBASE)/PGSIZE, 0);
uvmunmap(pagetable, (uint64)etext,(PHYSTOP-(uint64)etext)/PGSIZE, 0);
//kvmmap(KERNBASE, KERNBASE, (uint64)etext-KERNBASE, PTE_R | PTE_X);
uvmunmap(pagetable, TRAMPOLINE, 1, 0);
uvmunmap(pagetable, stack, 1,1 );
uvmfree(pagetable, 0);
}但是,这样做是不行的。
+Simplify
copyin/copyinstr
+参考:
+ +--会在这里卡住,会无限次不断进入kerneltrap。
-+
The kernel’s
copyin
function reads memory pointed to by user pointers. It does this by translating them to physical addresses, which the kernel can directly dereference. It performs this translation by walking the process page-table in software. Your job in this part of the lab is to add user mappings to each process’s kernel page table (created in the previous section) that allowcopyin
(and the related string functioncopyinstr
) to directly dereference user pointers.造成这个的原因,经过一番曲折的debug之后,我发现,只要像usertrap中的syscall分支一样:
-- -
if(r_scause() == 8){
// system call
// 重点是这里
// sepc points to the ecall instruction,
// but we want to return to the next instruction.
p->trapframe->epc += 4;
// ...
}加上这句话就行:
-+
sepc += 4;++Replace the body of
+copyin
inkernel/vm.c
with a call tocopyin_new
(defined inkernel/vmcopyin.c
); do the same forcopyinstr
andcopyinstr_new
. Add mappings for user addresses to each process’s kernel page table so thatcopyin_new
andcopyinstr_new
work.感想
这题很直观的思路是,在每个user pagetable添加映射的地方也添加kpgtbl的映射。但问题是,“每个user pagetable添加映射的地方”都是哪?
+误入幻想
我一开始想着偷偷懒,直接在proc.c和vm.c中每个操纵pagetable的地方都加上对kpgtbl的操纵。但很快我就给搞晕了。这时候,我心中萌生一计【PS:下面说的最后都没成功】:我直接快进到把proc结构中的pagetable属性给删了,然后每个出现p->pagetable的地方,都用p->kpgtbl代替,直接让两表合为一表,然后之后make的时候哪里报错改哪里,这不就一劳永逸地把所有出现pagetable的地方都改为kpgtbl了嘛。我振奋地去试了一下,将所有地方出现的pagetable都替换成了kpgtbl,把proc.c中的
+proc_pagetable()
和proc_freepagetable()
的出现的地方都换成了perproc_kvminit()
以及proc_freekpgtbl()
,还做了一个小细节,就是在userinit中调用的uvminit中,我把这样:-
void
uvminit(pagetable_t pagetable, uchar *src, uint sz)
{
char *mem;
if(sz >= PGSIZE)
panic("inituvm: more than a page");
mem = kalloc();
memset(mem, 0, PGSIZE);
mappages(pagetable, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);
memmove(mem, src, sz);
}所以,结果就非常显而易见了,是因为一直卡在这句话执行不下去:
-+
*(char*)va = 1;换成了这样:
+-
void
uvminit(struct proc* p, uchar *src, uint sz)
{
char *mem;
if(sz >= PGSIZE)
panic("inituvm: more than a page");
mem = kalloc();
memset(mem, 0, PGSIZE);
mappages(p->kpgtbl, 0, PGSIZE, (uint64)mem, PTE_W|PTE_R|PTE_X|PTE_U);
memmove(mem, src, sz);
}这是因为缺页中断会返回到原代码句中执行,所以就会继续回到这句话。而我们知道,此时正处于内核态,并没有开启地址映射,所以此处其实是非法地址越界了。但是我们的目的确实已经达到了(因为va通过stval寄存器被传递到了kerneltrap),所以我们这里只需跳过这句即可。
-这也是为什么说我这个方法虽然实现了,但本质上其实非常流氓,不算是kerneltrap。
+最后,在启动的时候,卡在了初次调度切换不到initcode这边,没有调用exec。没有panic,似乎只在死循环。我也实在想不出是什么原因,最后把代码删了【悲】想想我应该用git保存一下改前改后的。这下实在是难受了,我的想法也暂时没有机会实践了。等到明年大三说不定还得再交一次这玩意,到时候再探究探究吧hhh
+走上正途
发现这个最后没成还改了半天的我最后非常沮丧地去看了hints【又一心浮气躁耐心不足的表现,但确实绷不住了】,发现它居然说只用修改三个地方:fork、exec以及sbrk。
+我把kernel/下的每个文件都搜了一遍,发现确实,只有这三个,以及proc.c,vm.c,涉及到对页表项的增删。而在用户态中,想要对进程的内存进行管理,似乎只能通过系统调用sbrk。而proc.c和vm.c中确实没什么好改的。因为里面增加的映射,都是trapframe、trampoline、inicode这种不会一般在copyin中用到的虚拟地址。所以,要改的地方,确确实实,只有fork、exec以及sbrk。
--Update:验收的时候跟学长说了一下这个点,学长表示不算流氓,反而在内核(至少内核赛)中算是一个比较通用的手法hh没想到还误打误撞上了
-它带来的好处是,当地址不合法的时候可以减少开销。
-具体来说,内核中一般会将地址空间分为多个vma,因而检查地址越界无需像xv6那样简单查页表,只需查地址是否在对应的vma中即可。所以,直接把这东西转到一个硬件的缺页中断中实现,事实上确实是减少了地址非法时的开销。
+Xv6 applications ask the kernel for heap memory using the sbrk() system call.
除了这一点外,还有一点很重要的是,由于
-walkaddr
是需要返回一个pa的,因而我们需要手动再把pa在缺页中断后更新一下:+
pa = PTE2PA(*pte);
if((*pte & PTE_COW) != 0){
if((*pte & PTE_W) == 0){
*(char*)va = 1;
pa = PTE2PA(*pte);
}
}
return pa;很悲伤,我的初见思路是错误的()
+而这三个地方的共同点,就是都会对页表进行大量的copy。
+-
//in proc.c fork()
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}总之,做了这两个关键步骤后,也能启动了,也能过cowtest了。所以下面的代码也就贴上了这里的版本。
-心得
本次实验耗时经典五小时(包含笔记时间就是六个半小时了hhh),算是平均水平。很遗憾也很难受的一点是,我的错误最终还是没有自己想出来,而是参考了别人的代码才改对的。思路很简单,但是细节也依然非常多非常坑,还是得再加把劲。
-代码
我们只需要在fork的时候,标记父子进程的所有PTE都为read-only,然后之后就会遇到不同scause的缺页中断,针对特定的scause,新建物理页面,拷贝物理页面,然后重新设置映射即可。而对于其提出的需要标记某页是否能够释放,则需要统计每页的ref数,当ref==1的时候才可以释放。
-定义COW标记
+
// in kernel/riscv.h
// ...-
//in exec.c
// Commit to the user image.
oldpagetable = p->pagetable;
p->pagetable = pagetable;引用数组初始化
+
// in kernel/kalloc.c
char pages[(2<<19)];
struct spinlock pages_lock;
void
kinit()
{
initlock(&kmem.lock, "kmem");
initlock(&pages_lock,"pages");
memset(pages, 0, (2<<19));
freerange(end, (void*)PHYSTOP);
}
void
freerange(void *pa_start, void *pa_end)
{
char *p;
p = (char*)PGROUNDUP((uint64)pa_start);
for(; p + PGSIZE <= (char*)pa_end; p += PGSIZE){
pages[(uint64)p/PGSIZE] = 1;
kfree(p);
}
}-
//in syscall.c
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
if(growproc(n) < 0)
return -1;
return addr;
}
//in proc.c growproc()
uvmalloc(p->pagetable, sz, sz + n)) == 0申请和释放页时增删引用
+
// in kernel/kalloc.c
void *
kalloc(void)
{
struct run *r;
acquire(&kmem.lock);
r = kmem.freelist;
if(r)
kmem.freelist = r->next;
release(&kmem.lock);
if(r){
acquire(&pages_lock);
// 在这
pages[(uint64)r/PGSIZE] = 1;
release(&pages_lock);
}
if(r)
memset((char*)r, 5, PGSIZE); // fill with junk
return (void*)r;
}
void
kfree(void *pa)
{
struct run *r;
acquire(&pages_lock);
if(((uint64)pa % PGSIZE) != 0 || (char*)pa < end || (uint64)pa >= PHYSTOP || pages[(uint64)pa/PGSIZE] <= 0 )
panic("kfree");
// 每次kfree都会减少引用
pages[(uint64)pa/PGSIZE]--;
// 说明此时页面还被其他东西引用着,不能释放
if(pages[((uint64)pa)/PGSIZE] > 0){
release(&pages_lock);
return;
}
release(&pages_lock);
// Fill with junk to catch dangling refs.
memset(pa, 1, PGSIZE);
r = (struct run*)pa;
acquire(&kmem.lock);
r->next = kmem.freelist;
kmem.freelist = r;
release(&kmem.lock);
}所以,我们要做的事情很简单:写一个坐收渔翁之利的函数,内容为把一个页表的所有内容复制到另一个页表。然后再在这几个地方调用这个函数即可。
+代码
++注意:由于我写得实在是太烦了,已经思考不下去了。为了放过我自己,我写了个虽然能过得去测试但是其实毛病重重的代码。垃圾点为以下几点:
++
+- +
需要去掉freewalk中的panic
+我的kvmcopy的实现是,user pagetable(下面简称up)和tp的相同虚拟地址共用同一页物理内存。也就是说,页表不一样,但所指向的物理内存是同一个。这样设计的目的是为了能够让tp及时用到up的更新后的数据。
+这会导致啥呢?在进程释放时,需要一起调用
+proc_freepagetable
和proc_freekpgtbl
。proc_freepagetable
调用完后,所指向的那堆物理内存已经寄完了,如果再调用proc_freekpgtbl
,显然,就会发生页表未释放但页表对应内存已经释放的问题,freewalk就会panic。因此,我简单粗暴地直接把freewalk的panic删掉了【抖】也许有别的解决方法,但我真是烦得不想想了放过我吧(- +
好像暂时没有第二点了()
+渔翁之利函数
-
// in vm.c
// 效仿的是vm.c中的uvmcopy
int
kvmcopy(pagetable_t up, pagetable_t kp, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(up, i, 0)) == 0 || (*pte & PTE_V) == 0){
if(walk(kp,i,0) == 0){
//如果up不存在此项,kp存在,就直接删了
uvmunmap(kp,i,PGSIZE,0);
}
continue;
}
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
// 注意去除PTE_U,否则内核态无法访问
flags = (flags & (~PTE_U));
if(mappages(kp, i, PGSIZE, pa, flags) != 0){
goto err;
}
}
return 0;
err:
uvmunmap(kp, 0, i / PGSIZE, 1);
return -1;
}修改fork时对页表的复制操作,并标记引用数增加
+
// in kernel/proc.c fork()
// Copy user memory from parent to child.
if(cowcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
// in kernel/vm.c
int
cowcopy(pagetable_t old, pagetable_t new, uint64 sz)
{
pte_t *pte;
uint64 pa, i;
uint flags;
for(i = 0; i < sz; i += PGSIZE){
if((pte = walk(old, i, 0)) == 0)
panic("cowcopy: pte should exist");
if((*pte & PTE_V) == 0)
panic("cowcopy: page not present");
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
// 去除flag中的PTE_W,并且给父子的都安上没有PTE_W的flag
flags = (flags & (~PTE_W));
flags = (flags | PTE_COW);
*pte = ((*pte) & (~PTE_W));
*pte = ((*pte) | PTE_COW);
if(mappages(new, i, PGSIZE, (uint64)pa, flags) != 0){
goto err;
}
// 标记物理页的引用数增加
acquire(&pages_lock);
pages[pa/PGSIZE]++;
release(&pages_lock);
}
return 0;
err:
// 失败了不能释放物理内存
uvmunmap(new, 0, i / PGSIZE, 0);
return -1;
}修改fork、exec、sbrk
fork
-
// in proc.c fork()
// Copy user memory from parent to child.
if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}
if(kvmcopy(np->pagetable, np->kpgtbl, p->sz) < 0){
freeproc(np);
release(&np->lock);
return -1;
}处理缺页中断,标记引用数减少
+
} else if(r_scause() == 15){
// 只要求写入引起的缺页中断
uint64 va = r_stval();
pte_t *pte;
uint64 pa;
uint flags;
if((pte = walk(p->pagetable, va, 0)) == 0)
p->killed = 1;
else if((*pte & PTE_V) == 0)
p->killed = 1;
else {
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
char* mem;
if((mem = kalloc())!=0){
memmove(mem, (char*)pa, PGSIZE);
// 设置为新的物理页地址
*pte = PA2PTE(mem);
// 减少引用,引用归零时释放
kfree((void*)pa);
// 设置新的flag,标记为可写
flags = (flags | PTE_W | PTE_COW);
*pte = ((*pte) | flags);
} else{
p->killed = 1;
}
}
}exec
-
// in exec.c
// Commit to the user image.
oldpagetable = p->pagetable;
p->pagetable = pagetable;
p->sz = sz;
p->trapframe->epc = elf.entry; // initial program counter = main
p->trapframe->sp = sp; // initial stack pointer
proc_freepagetable(oldpagetable, oldsz);
// 添上此句
kvmcopy(p->pagetable, p->kpgtbl, p->sz);uvmunmap时减少引用数
+
void
uvmunmap(pagetable_t pagetable, uint64 va, uint64 npages, int do_free)
{
uint64 a;
pte_t *pte;
// ...
for(a = va; a < va + npages*PGSIZE; a += PGSIZE){
// ...
uint64 pa = PTE2PA(*pte);
if(do_free){
kfree((void*)pa);
}else {
acquire(&pages_lock);
if(((*pte) & PTE_COW) != 0){
pages[pa/PGSIZE]--;
}
release(&pages_lock);
}
*pte = 0;
}
}sbrk
-
uint64
sys_sbrk(void)
{
int addr;
int n;
if(argint(0, &n) < 0)
return -1;
addr = myproc()->sz;
if(addr+n >= PLIC) return -1;
if(growproc(n) < 0)
return -1;
return addr;
}修改walkaddr
在walkaddr中触发缺页中断
+
uint64
walkaddr(pagetable_t pagetable, uint64 va)
{
pte_t *pte;
uint64 pa;
if(va >= MAXVA)
return 0;
pte = walk(pagetable, va, 0);
if(pte == 0)
return 0;
if((*pte & PTE_V) == 0)
return 0;
if((*pte & PTE_U) == 0)
return 0;
pa = PTE2PA(*pte);
// 在这里
if((*pte & PTE_COW) != 0){
if((*pte & PTE_W) == 0){
// 触发缺页中断
*(char*)va = 1;
// 更新pa值
pa = PTE2PA(*pte);
}
}
return pa;
}-
// in proc.c
// Grow or shrink user memory by n bytes.
// Return 0 on success, -1 on failure.
int
growproc(int n)
{
uint sz;
struct proc *p = myproc();
sz = p->sz;
// ...
p->sz = sz;
// 加这个
kvmcopy(p->pagetable, p->kpgtbl, p->sz);
return 0;
}在kerneltrap内补上对缺页中断的处理
+
void
kerneltrap()
{
uint64 sepc = r_sepc();
// ...
if(r_scause() == 15){
// 只要写入引起的缺页中断
uint64 va = r_stval();
pte_t *pte;
uint64 pa;
uint flags;
if((pte = walk(p->pagetable, va, 0)) == 0)
p->killed = 1;
else if((*pte & PTE_V) == 0)
p->killed = 1;
else {
// 注意,这个很重要!!!!!
sepc += 4;
pa = PTE2PA(*pte);
flags = PTE_FLAGS(*pte);
char* mem;
if((mem = kalloc())!=0){
memmove(mem, (char*)pa, PGSIZE);
// 设置为新的物理页地址
*pte = PA2PTE(mem);
kfree((void*)pa);
// 设置新的flag,标记为可写
flags = (flags | PTE_W | PTE_COW);
*pte = ((*pte) | flags);
} else{
p->killed = 1;
}
}
} else if((which_dev = devintr()) == 0){
printf("scause %p\n", scause);
printf("sepc=%p stval=%p\n", r_sepc(), r_stval());
panic("kerneltrap");
}
// ...
}userinit
++这一步不能忽视,因为内核启动的时候就需要用到copyinstr。
++
// in proc.c userinit()
uvminit(p->pagetable, initcode, sizeof(initcode));
p->sz = PGSIZE;
// 加这个!
kvmcopy(p->pagetable, p->kpgtbl, p->sz);删掉freewalk的panic(我特有的缺点)
]]>
// in vm.c freewalk()
if((pte & PTE_V) && (pte & (PTE_R|PTE_W|PTE_X)) == 0){
// ...
} else if(pte & PTE_V){
//panic("freewalk: leaf");
}
http://localhost/webdemo4_war/*.do
。
之后写学校实验时回过头来看,发现之前的实现是不对的,在同时进入bfree
函数时有死锁风险。经过修改后虽然粒度大了但是安全了。对了,额外附上一版不知道为啥错了的细粒度版本……看了真感觉没什么问题,但依然是会在bfree
时panic两次free。等以后有精力再继续研究吧(泪目)
错误版本的思路就是,使用每个block块自己的锁(b->lock
)和每个桶的锁来实现细粒度加锁。我是左看右看感觉每个block从在bget中获取一直到brelse释放的b->lock
锁是一直持有的,但确实依然有可能发生两个进程同时获取同一个block的锁的情况。实在不知道怎么办了,想了很久还是没想出细粒度好方法(泪)总之代码先放在这里。
请见我的github。
-static struct buf* |
请见我的github。
+static struct buf* |
void |
++来到指导书最高点!太美丽了xv6。哎呀那不文件系统吗(
+这里是自底向上讲起的。之后可以看看hit网课的自顶向下。
+
+++
++The disk layer reads and writes blocks on an virtio hard drive.
+The buffer cache layer caches disk blocks and synchronizes access to them, making sure that only one kernel process at a time can modify the data stored in any particular block.
+The logging layer allows higher layers to wrap updates to several blocks in a transaction, and ensures that the blocks are updated atomically in the face of crashes (i.e., all of them are updated or none). 【日志记录层允许更高层将更新包装到一个事务中的多个块,并确保在崩溃时以原子方式更新块(即,所有块都更新或不更新)。可以类比一下数据库的那个概念。】
+The inode layer provides individual files, each represented as an inode with a unique i-number and some blocks holding the file’s data.
+The directory layer implements each directory as a special kind of inode whose content is a sequence of directory entries, each of which contains a file’s name and i-number.
+The pathname layer provides hierarchical path names like /usr/rtm/xv6/fs.c, and resolves them with recursive lookup.
+The file descriptor layer abstracts many Unix resources (e.g., pipes, devices, fifiles, etc.) using the file system interface, simplifying the lives of application programmers.
+
++The file system must have a plan for where it stores inodes and content blocks on the disk. To do so, xv6 divides the disk into several sections, as Figure 8.2 shows.
+The file system does not use block 0 (it holds the boot sector).
+Block 1 is called the superblock; it contains metadata about the file system (the file system size in blocks, the number of data blocks, the number of inodes, and the number of blocks in the log). The superblock is filled in by a separate program, called
+mkfs
, which builds an initial file system.Blocks starting at 2 hold the log.
+After the log are the inodes, with multiple inodes per block.
+After those come bitmap blocks tracking which data blocks are in use. 【应该是用来标识每个块是否空闲的吧】
+The remaining blocks are data blocks; each is either marked free in the bitmap block, or holds content for a file or directory【要么空闲要么是文件或目录】.
+
++The buffer cache has two jobs:
++
+- synchronize access to disk blocks to ensure that only one copy of a block is in memory and that only one kernel thread at a time uses that copy;
+- cache popular blocks so that they don’t need to be re-read from the slow disk.
+The code is in
+bio.c
.Buffer cache中保存磁盘块的缓冲区数量固定,这意味着如果文件系统请求还未存放在缓存中的块,Buffer cache必须回收当前保存其他块内容的缓冲区。Buffer cache为新块回收最近使用最少的缓冲区。这样做的原因是认为最近使用最少的缓冲区是最不可能近期再次使用的缓冲区。
+
struct buf { |
这应该代表着一个磁盘块。
+struct { |
大概buf数组里存储着所有buf的内容。buf本身通过最近使用排序的双向链表连接,head是链表的头。
+// called by main.c |
++The main interface exported by the buffer cache consists of
+bread
andbwrite
.The buffer cache uses a per-buffer sleep-lock to ensure concurrent security.
+
+++
bread
obtains a buf containing a copy of a block which can be read or modified in memory.依据给定设备号和给定扇区号寻找cache的buf。返回的buf是locked的。
+
// Return a locked buf with the contents of the indicated block. |
++writes a modified buffer to the appropriate block on the disk
+
// Write b's contents to disk. Must be locked. |
++A kernel thread must release a buffer by calling brelse when it is done with it.
+
// Release a locked buffer. |
用于获取cache中是否存在block。如果不存在,则新申请一个buf,并把该buf以上锁状态返回
+// Look through buffer cache for block on device dev. |
++Xv6通过简单的日志记录形式解决了文件系统操作期间的崩溃问题。
+xv6系统调用不会直接写入磁盘上的文件系统数据结构。相反,它会在磁盘上的log(日志)中放置它希望进行的所有磁盘写入的描述。一旦系统调用记录了它的所有写入操作,它就会向磁盘写入一条特殊的commit(提交)记录,表明日志包含一个完整的操作。此时,系统调用将写操作复制到磁盘上的文件系统数据结构。完成这些写入后,系统调用将擦除磁盘上的日志。
+
++如果系统崩溃并重新启动,则在运行任何进程之前,文件系统代码将按如下方式从崩溃中恢复:
+如果日志标记为包含完整操作,则恢复代码会将写操作复制到磁盘文件系统中它们所属的位置,然后擦除日志。如果日志没有标记为包含完整操作,则恢复代码将忽略该日志,然后擦除日志。
+
这就保证了原子性。
+superblock记录了log的存储位置。
+++它由一个头块(header block)和一系列更新块的副本(logged block)组成。
+头块包含一个扇区号(sector)数组(每个logged block对应一个扇区号)以及日志块的计数。
+磁盘上的头块中的计数为零表示日志中没有事务,为非零表示日志包含一个完整的已提交事务,并具有指定数量的logged block。
+在事务提交(commit)时Xv6才向头块写入数据,在此之前不会写入。在将logged blocks复制到文件系统后,头块的计数将被设置为零。
+因此,事务中途崩溃将导致日志头块中的计数为零;提交后的崩溃将导致非零计数。
+
++为了允许不同进程并发执行文件系统操作,日志系统可以将多个系统调用的写入累积到一个事务中。因此,单个提交可能涉及多个完整系统调用的写入。为了避免在事务之间拆分系统调用,日志系统仅在没有文件系统调用进行时提交。
+同时提交多个事务的想法称为组提交(group commit)。组提交减少了磁盘操作的数量,因为成本固定的一次提交分摊了多个操作。组提交还同时为磁盘系统提供更多并发写操作,可能允许磁盘在一个磁盘旋转时间内写入所有这些操作。Xv6的virtio驱动程序不支持这种批处理,但是Xv6的文件系统设计允许这样做。
+【这感觉实现得也还挺简略的】
+
++Xv6在磁盘上留出固定的空间来保存日志。事务中系统调用写入的块总数必须可容纳于该空间。这导致两个后果:
++
+- +
任何单个系统调用都不允许写入超过日志空间的不同块。
+【这段话我一个字没看懂】
+这对于大多数系统调用来说都不是问题,但其中两个可能会写入许多块:
+write
和unlink
。一个大文件的write
可以写入多个数据块和多个位图块以及一个inode块;unlink
大文件可能会写入许多位图块和inode。Xv6的write
系统调用将大的写入分解为适合日志的多个较小的写入,unlink
不会导致此问题,因为实际上Xv6文件系统只使用一个位图块。- +
日志空间有限的另一个后果是,除非确定系统调用的写入将可容纳于日志中剩余的空间,否则日志系统无法允许启动系统调用。
+
++log的原理是这样的:
+在每个系统调用的开始调用
+begin_op
表示事务开始,然后之后新申请一块block,也即把该block的内容读入内存,并且把该block的blockno记录到log的header中。此后程序正常修改在内存中的block,磁盘中的block保持不变。最后commit的时候遍历log header中的blockno,一块块地把内存中的block写入日志和磁盘中。如果程序在commit前崩溃,则内存消失,同时磁盘也不会写入;如果在commit后崩溃,那也无事发生。
+在每次启动的时候,都会执行log的初始化,届时可以顺便恢复数据。
+完美实现了日志的功能。
+
// Contents of the header block, used for both the on-disk header block |
void |
+++
begin_op
等待直到日志系统当前未处于提交中,并且直到有足够的未被占用的日志空间来保存此调用的写入。+
log.outstanding
统计预定了日志空间的系统调用数;为此保留的总空间为log.outstanding
乘以MAXOPBLOCKS
(10)。递增log.outstanding
会预定空间并防止在此系统调用期间发生提交(if的第二个分支)。代码保守地假设每个系统调用最多可以写入MAXOPBLOCKS
(10)个不同的块。
// called at the start of each FS system call. |
xv6中,每个CPU中的scheduler都有一个专用的线程。这里线程的概念是,有自己的栈,有自己的寄存器状态。
-当发生时钟中断时,当前进程调用yield,yield再通过swtch切换到scheduler线程。scheduler线程会通过swtch跳转到另外一个进程去执行。当另外一个进程发生时钟中断,又会通过yield回到scheduler,scheduler再调度原进程继续执行,如此周而复始。
--Linux的调度原理也差不多类似这样。每个CPU都有一个调度类为SCHED_CLASS_IDLE的IDLE进程,IDLE进程体大概就是间歇不断地执行__schedule()函数,CPU空闲时就会不断执行IDLE线程。
-而当有新的任务产生时(或任务被唤醒。可以从此看出task new和task wakeup的共通点,可以联想到竞赛中对该消息的处理方法),它首先通过调度类对应的select_cpu选择一个合适的(可以被抢占&&在该task对应的cpumask中)的cpu,迁移到cpu对应的rq;目标cpu通过IDLE进程体或者中断返回时检查到了NEED_SCHEDULE标记位,从而调用schedule函数pick新任务,然后进行context_switch切换到目标线程。如此周而复始。
+log_write
+-+
log_write
充当bwrite
的代理。它将块的扇区号记录在内存中,在磁盘上的日志中预定一个槽位,并调用bpin
将缓存固定在block cache中,以防止block cache将其逐出【具体原理就是让refcnt++,这样就不会被当成空闲block用掉了】。为啥要防止换出呢?换出不是就正好自动写入磁盘了吗?这里一是为了保障前面提到的原子性,防止换入换出导致的单一写入磁盘;二是换出自动写入的是磁盘对应位而不一定是日志所在的blocks。
-
下面就来讲讲这个所谓的“线程”以及xv6的上下文切换是怎么实现的。
-context
上下文切换的操作对象是上下文,因而首先了解一下上下文的结构。各种寄存器的状态即是上下文context。xv6中的context定义如下:
-+
struct context {
uint64 ra;
uint64 sp;
// callee-saved
uint64 s0;
uint64 s1;
uint64 s2;
uint64 s3;
uint64 s4;
uint64 s5;
uint64 s6;
uint64 s7;
uint64 s8;
uint64 s9;
uint64 s10;
uint64 s11;
};-
void
log_write(struct buf *b)
{
int i;
// #define LOGSIZE (MAXOPBLOCKS*3) // max data blocks in on-disk log
// 30
if (log.lh.n >= LOGSIZE || log.lh.n >= log.size - 1)
panic("too big a transaction");
if (log.outstanding < 1)
panic("log_write outside of trans");
acquire(&log.lock);
// log_write会注意到在单个事务中多次写入一个块的情况,并在日志中为该块分配相同的槽位。
// 这种优化通常称为合并(absorption)
for (i = 0; i < log.lh.n; i++) {
if (log.lh.block[i] == b->blockno) // log absorbtion
break;
}
// 这里还是挺巧妙的。
// 如果存在log.lh.block[i] == b->blockno的情况,执行此句话也无妨
// 如果不存在,则给log新增一块,填入log.lh.block[log.lh.n]的位置,再++log.lh.n
log.lh.block[i] = b->blockno;
if (i == log.lh.n) { // Add new block to log?
bpin(b);
log.lh.n++;
}
release(&log.lock);
}上下文切换需要修改栈和pc,context中确实有sp寄存器,但是没有pc寄存器,这主要还是因为当swtch返回时,会回到ra所指向的地方,所以仅保存ra就足够了。
-swtch
上下文的切换是通过swtch实现的。
-+
void swtch(struct context*, struct context*);end_op
-
// called at the end of each FS system call.
// 如果这是最后一层outstanding就会执行commit操作
// commits if this was the last outstanding operation.
void
end_op(void)
{
int do_commit = 0;
acquire(&log.lock);
log.outstanding -= 1;
if(log.committing)
panic("log.committing");
if(log.outstanding == 0){
do_commit = 1;
log.committing = 1;
} else {
// begin_op() may be waiting for log space,
// and decrementing log.outstanding has decreased
// the amount of reserved space.
wakeup(&log);
}
release(&log.lock);
if(do_commit){
// call commit w/o holding locks, since not allowed
// to sleep with locks.
commit();
acquire(&log.lock);
log.committing = 0;
wakeup(&log);
release(&log.lock);
}
}swtch会把当前进程的上下文保存在第一个context中,再切换到第二个context保存的上下文,具体实现就是写读保存寄存器:
-+
# in kernel/swtch.S
# a0和a1分别保存着两个参数的值,也即第一个context的地址和第二个context的地址
.globl swtch
swtch:
sd ra, 0(a0)
sd sp, 8(a0)
sd s0, 16(a0)
sd s1, 24(a0)
# ...
sd s11, 104(a0)
ld ra, 0(a1)
ld sp, 8(a1)
ld s0, 16(a1)
ld s1, 24(a1)
# ...
ld 11, 104(a1)
retcommit
-
static void
commit()
{
if (log.lh.n > 0) {
// cache -> log block
write_log(); // Write modified blocks from cache to log
// head(in stack/heap) -> log block
// 此可以说为commit完成的标志。
// 因为无论接下来是否崩溃,数据最终都会被写入disk,不同在于是在recover时还是接下来写入
write_head(); // Write header to disk -- the real commit
// log block -> real position
install_trans(0); // Now install writes to home locations
log.lh.n = 0;
// 擦除
write_head(); // Erase the transaction from the log
}
}sched
在sleep、yield和wakeup中,都会通过sched中的swtch进入scheduler线程。
-+
void
sched(void)
{
int intena;
struct proc *p = myproc();
if(!holding(&p->lock))
panic("sched p->lock");
if(mycpu()->noff != 1)
panic("sched locks");
if(p->state == RUNNING)
panic("sched running");
if(intr_get()) // 当持有锁时一定为关中断状态
panic("sched interruptible");
intena = mycpu()->intena;
swtch(&p->context, &mycpu()->context);
mycpu()->intena = intena;
}write_log
-
// Copy modified blocks from cache to log.
static void
write_log(void)
{
int tail;
for (tail = 0; tail < log.lh.n; tail++) {
struct buf *to = bread(log.dev, log.start+tail+1); // log block
struct buf *from = bread(log.dev, log.lh.block[tail]); // cache block
memmove(to->data, from->data, BSIZE);
bwrite(to); // write the log
brelse(from);// 此处的brelse呼应了外界调用的bread
brelse(to);
}
}cpu中存储着的是scheduler线程的context。因而,这样就可以保存当前进程的context,读取scheduler线程的context,然后转换到scheduler的context执行了。
+write_head
+ +
// Write in-memory log header to disk.
// 这是事务提交的标志
// This is the true point at which the
// current transaction commits.
static void
write_head(void)
{
struct buf *buf = bread(log.dev, log.start);
struct logheader *hb = (struct logheader *) (buf->data);
int i;
hb->n = log.lh.n;
for (i = 0; i < log.lh.n; i++) {
hb->block[i] = log.lh.block[i];
}
bwrite(buf);
brelse(buf);
}install_trans
+ +
// Copy committed blocks from log to their home location
static void
install_trans(int recovering)
{
int tail;
for (tail = 0; tail < log.lh.n; tail++) {
struct buf *lbuf = bread(log.dev, log.start+tail+1); // read log block
struct buf *dbuf = bread(log.dev, log.lh.block[tail]); // read dst
memmove(dbuf->data, lbuf->data, BSIZE); // copy block to dst
bwrite(dbuf); // write dst to disk
if(recovering == 0)
bunpin(dbuf);// 如果不是在recover的过程中
brelse(lbuf);
brelse(dbuf);
}
}恢复与初始化
上面介绍了log的一次事务提交的流程。接下来介绍它是怎么恢复的。
--可以发现这里是有个很完美的组合技的。由sched()保存context到process结构体中,再由scheduler()读取process对应的context回归到sched()继续执行,我感觉调度设计这点真是帅的一匹。
+
recover_from_log
是由initlog
调用的,而它又是在第一个用户进程运行之前的引导期间由fsinit
调用的。scheduler
+
void
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();
c->proc = 0;
for(;;){
// Avoid deadlock by ensuring that devices can interrupt.
intr_on();
int nproc = 0;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state != UNUSED) {
nproc++;
}
if(p->state == RUNNABLE) {
// Switch to chosen process. It is the process's job
// to release its lock and then reacquire it
// before jumping back to us.
p->state = RUNNING;
c->proc = p;
swtch(&c->context, &p->context);
// Process is done running for now.
// It should have changed its p->state before coming back.
c->proc = 0;
}
release(&p->lock);
}
if(nproc <= 2) { // only init and sh exist
intr_on();
asm volatile("wfi");
}
}
}第一个进程运行之前
由前面scheduler一章的知识可知,每个进程被初次调度的时候会先来执行
+forkret
。这时候就做了log的恢复工作。注释解释了为什么不选择在
+main.c
中初始化,而选择在此处初始化。确实,它需要调用sleep,如果在main.c中调用sleep感觉会乱套()毕竟那时候scheduler线程尚未被初始化。-
// A fork child's very first scheduling by scheduler()
// will swtch to forkret.
void
forkret(void)
{
// static变量仅会被初始化一次
static int first = 1;
// Still holding p->lock from scheduler.
release(&myproc()->lock);
// 如果是第一个进程
if (first) {
// File system initialization must be run in the context of a
// regular process (e.g., because it calls sleep), and thus cannot
// be run from main().
first = 0;
fsinit(ROOTDEV);
}
usertrapret();
}通过swtch进入scheduler线程后,会继续执行scheduler中swtch的下一个指令,完成下一次调度。
-一些补充
以上是书本的介绍内容。看到这想必会有很多疑惑,至少有以下两点:
--
-- 为什么cpu->context会存储着scheduler的上下文?这是什么时候,又是怎么初始化的?
-- 为什么从sched中swtch会来到scheduler中swtch的下一句?
-先从第一点入手。实际上,这个初始化的工作,是在操作系统启动时的
-main.c
中完成的。+
void
main()
{
if(cpuid() == 0){
// ...
} else {
// ...
}
scheduler();
}fsinit
-
// Init fs
void
fsinit(int dev) {
// ...
initlog(dev, &sb);
}在这之前,创建了第一个进程proc。在这里,每个cpu都调用了scheduler。
-+
void
scheduler(void)
{
struct proc *p;
struct cpu *c = mycpu();
c->proc = 0;
for(;;){
intr_on();
int nproc = 0;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
// ...
if(p->state == RUNNABLE) {
p->state = RUNNING;
c->proc = p;
swtch(&c->context, &p->context);
c->proc = 0;
}
release(&p->lock);
}
// ...
}
}initlog
-
void
initlog(int dev, struct superblock *sb)
{
if (sizeof(struct logheader) >= BSIZE)
panic("initlog: too big logheader");
initlock(&log.lock, "log");
// 从super block中获取必要参数
log.start = sb->logstart;
log.size = sb->nlog;
log.dev = dev;
recover_from_log();
}每个cpu都在scheduler线程的
-swtch(&c->context, &p->context);
中,将当前的context,也即scheduler的context存入了mycpu()->context
。随后,CPU中的某个去执行下一个进程,其他的就在scheduler线程的无限循环中等待,直到有别的进程产生。去执行进程的CPU通过swtch切换上下文,切到了另一个进程中,此时在swtch中保存的ra是scheduler线程的swtch的下一句(因为scheduler->swtch也是个函数调用的过程)。会切到另一个进程的sched的下一句【因为它正是从那边swtch过来的】,或者是那个进程开始执行的地方【下面会提到是forkret】。另一个进程通过sched切换回来的时候,就正会切到ra所指向的位置,也即切到scheduler中的swtch后面。
-这样一来,两个问题就都得到了解答。
-从这,我们也能知道xv6是如何让CPU运转的:scheduler线程是CPU的IDLE状态。无事的时候在scheduler中等待,并且一直监测是否有进程需要执行。有的话,则去执行该进程;该进程又会通过sched切换回scheduler线程,继续等待。这样一来,就完成了进程管理的基本的自动机图像。
-Code: Scheduling
sched前要做的事
-A process that wants to give up the CPU must do three things:
+recover_from_log
+ +
static void
recover_from_log(void)
{
// 读取head
read_head();
// 注意,commit中会把header写入log block,而这里从log block读出header
// 也就是说,如果header的n不为零,那么说明已经commit了,但可能未写入,重复写入保障安全
// 如果header的n为零,说明未commit,在install_trans的逻辑中会什么也不做
// 两种情况完美满足
install_trans(1); // if committed, copy from log to disk
log.lh.n = 0;
// 擦除
write_head(); // clear the log
}Code: Block allocator
个人理解
说实话没怎么懂,也不大清楚它有什么用,先大概推测一下:
+之前的bread和bwrite这些,就是你给一个设备号和扇区号,它就帮你加载进内存cache。你如果要用的话,肯定还是使用地址方便。所以block allocator的作用之一就是给bread和bwrite加一层封装,将获取的block封装为地址返回,你可以直接操纵这个地址,而无需知道下层的细节。
+这个过程要注意的有两点:
-
-- acquire its own process lock p->lock, release any other locks it is holding
-- update its own state (p->state)
-- call sched
+- +
封装返回的地址具体是什么,怎么工作的
+封装返回的地址实质上是buffer cache中的buf的data字段的地址【差不多】。之后的上层应用在该地址上写入,也即写入了buf,最后会通过log层真正写入磁盘。
+结合bcache的LRU,详细谈谈工作机制
+我们可以看到,在balloc中有这么一段逻辑:
++ +
bp = bread(dev, BBLOCK(b, sb));
// ...
log_write(bp);
brelse(bp);
return b + bi;看到的第一反应就是,我们需求的那块buf是bp,但是这里先是bread了一次,又是brelse了一次,这样bp的refcnt不就为0,很容易被替换掉了吗?
+会有这个反应,一定程度上是因为没有很好地理解LRU。事实上,正是它可能被替换掉,才满足了LRU的条件。因为它可能被替掉才能说明它可能是最近最少使用的。
+-
yield
(kernel/proc.c:515) follows this convention, as dosleep
andexit
.+
sched
double-checks those conditions (kernel/proc.c:499-504) and then an implication of those conditions: since a lock is held, interrupts should be disabled.bitmap
+-文件和目录内容存储在磁盘块中,磁盘块必须从空闲池中分配。xv6的块分配器在磁盘上维护一个空闲位图,每一位代表一个块。0表示对应的块是空闲的;1表示它正在使用中。
+引导扇区、超级块、日志块、inode块和位图块的比特位是由程序
+mkfs
初始化设置的:
sched与scheduler
在上面的描述我们可以看到,
-sched
和scheduler
联系非常密切,他们俩通过swtch
相互切来切去,并且一直都只在这几行切来切去:+
// in scheduler()
swtch(&c->context, &p->context);
c->proc = 0;
// in sched()
swtch(&p->context, &mycpu()->context);
mycpu()->intena = intena;allocator
类似于memory allocator,块分配器也提供了两个函数:
+bfree
和balloc
。balloc
+++
Balloc
从块0到sb.size
(文件系统中的块数)遍历每个块。它查找位图中位为零的空闲块。如果balloc
找到这样一个块,它将更新位图并返回该块。为了提高效率,循环被分成两部分。外部循环读取位图中的每个块。内部循环检查单个位图块中的所有BPB位。由于任何一个位图块在buffer cache中一次只允许一个进程使用【
+bread(dev, BBLOCK(b, sb))
会返回一个上锁的block,bread
和brelse
隐含的独占使用避免了显式锁定的需要】,因此,如果两个进程同时尝试分配一个块也是并发安全的。-
// Allocate a zeroed disk block.
static uint
balloc(uint dev)
{
int b, bi, m;
struct buf *bp;
bp = 0;
for(b = 0; b < sb.size; b += BPB){
bp = bread(dev, BBLOCK(b, sb));
for(bi = 0; bi < BPB && b + bi < sb.size; bi++){
m = 1 << (bi % 8);
if((bp->data[bi/8] & m) == 0){ // Is block free?
bp->data[bi/8] |= m; // Mark block in use.
log_write(bp);
brelse(bp);
bzero(dev, b + bi);
return b + bi;
}
}
brelse(bp);
}
panic("balloc: out of blocks");
}在两个线程之间进行这种样式化切换的过程有时被称为协程(coroutines)。
--存在一种情况使得调度程序对
+swtch
的调用没有以sched
结束。一个新进程第一次被调度时,它从forkret
(kernel/proc.c:527)开始。Forkret
是为了释放p->lock
而包装的,要不然,新进程可以从usertrapret
开始。bfree
+ +
// Free a disk block.
static void
bfree(int dev, uint b)
{
struct buf *bp;
int bi, m;
bp = bread(dev, BBLOCK(b, sb));
bi = b % BPB;
m = 1 << (bi % 8);
if((bp->data[bi/8] & m) == 0)
panic("freeing free block");
bp->data[bi/8] &= ~m;
log_write(bp);
brelse(bp);
}Inode layer
inode
+-术语inode(即索引结点)可以具有两种相关含义之一。它可能是指包含文件大小和数据块编号列表的磁盘上的数据结构【on-disk inode】。或者“inode”可能指内存中的inode【in-memory inode】,它包含磁盘上inode的副本以及内核中所需的额外信息。
p->lock保证了并发安全性
-考虑调度代码结构的一种方法是,它为每个进程强制维持一个不变性条件的集合,并在这些不变性条件不成立时持有
-p->lock
。其中一个不变性条件是:如果进程是
-RUNNING
状态,计时器中断的yield
必须能够安全地从进程中切换出去;这意味着CPU寄存器必须保存进程的寄存器值(即swtch
没有将它们移动到context
中),并且c->proc
必须指向进程。另一个不变性条件是:如果进程是RUNNABLE
状态,空闲CPU的调度程序必须安全地运行它;这意味着p->context
必须保存进程的寄存器(即,它们实际上不在实际寄存器中),没有CPU在进程的内核栈上执行,并且没有CPU的c->proc
引用进程。维护上述不变性条件是xv6经常在一个线程中获取p->lock并在另一个线程中释放它的原因,在保持
-p->lock
时,这些属性通常不成立。例如在
-yield
中获取并在scheduler
中释放。一旦yield
开始修改一个RUNNING
进程的状态为RUNNABLE
,锁必须保持被持有状态,直到不变量恢复:最早的正确释放点是scheduler
(在其自身栈上运行)清除c->proc
之后。类似地,一旦scheduler
开始将RUNNABLE
进程转换为RUNNING
,在内核线程完全运行之前(在swtch
之后,例如在yield
中)绝不能释放锁。+
p->lock
还保护其他东西:exit
和wait
之间的相互作用,避免丢失wakeup
的机制(参见第7.5节),以及避免一个进程退出和其他进程读写其状态之间的争用(例如,exit
系统调用查看p->pid
并设置p->killed
(kernel/proc.c:611))。为了清晰起见,也许为了性能起见,有必要考虑一下p->lock
的不同功能是否可以拆分。+
on-disk inode
+-The on-disk inodes are packed into a contiguous area of disk called the inode blocks.
+Every inode is the same size, so it is easy, given a number n, to find the nth inode on the disk. In fact, this number n, called the inode number or i-number, is how inodes are identifified in the implementation.
p->lock在每次scheduler开始的时候获取,swtch到p进程的时候在yield等调用完sched的地方释放。而调用yield时获取的锁,又会在scheduler中释放。
-+
// Give up the CPU for one scheduling round.
void
yield(void)
{
struct proc *p = myproc();
acquire(&p->lock);// 该锁会在scheduler中释放
p->state = RUNNABLE;
sched();
release(&p->lock);// 该锁释放的是scheduler中得到的锁
}-
// in fs.h
// On-disk inode structure
struct dinode {
// 为0表示free
short type; // File type
short major; // Major device number (T_DEVICE only)
short minor; // Minor device number (T_DEVICE only)
// The nlink field counts the number of directory entries that refer to this inode,
// in order to recognize when the on-disk inode and its data blocks should be freed.
short nlink; // Number of links to inode in file system
uint size; // Size of file (bytes)
uint addrs[NDIRECT+1]; // Data block addresses
};+
// in kernel/proc.c scheduler()
acquire(&p->lock);// 该锁会在yield等地被释放
// ...
swtch(&c->context, &p->context);
// ...
release(&p->lock);// 该锁会释放yield等地中获得的锁in-memory inode
++The kernel keeps the set of active inodes in memory.
+The kernel stores an inode in memory only if there are C pointers referring to that inode.当且仅当ref==0才会从内核中释放。
+如果nlinks==0就会从物理block中释放。
+The
+iget
andiput
functions acquire and release pointers to an inode, modifying the reference count.【相当于buffer cache的balloc
和bfree
】Pointers to an inode can come from file descriptors, current working directories, and transient kernel code such as exec.+
iget
返回的struct inode
可能没有任何有用的内容。为了确保它保存磁盘inode的副本,代码必须调用ilock
。这将锁定inode(以便没有其他进程可以对其进行ilock
),并从磁盘读取尚未读取的inode。iunlock
释放inode上的锁。将inode指针的获取与锁定分离有助于在某些情况下避免死锁,例如在目录查找期间。多个进程可以持有指向iget
返回的inode的C指针,但一次只能有一个进程锁定inode。-
//in file.h
// in-memory copy of an inode
struct inode {
uint dev; // Device number
uint inum; // Inode number
int ref; // Reference count
struct sleeplock lock; // protects everything below here
int valid; // inode has been read from disk?
short type; // copy of disk inode
short major;
short minor;
short nlink;
uint size;
uint addrs[NDIRECT+1];// 存储着inode数据的blocks的地址,从balloc中获取
};不得不说,这结构实在是太精妙了。这中间的如此多的复杂过程,就这样成功地被锁保护了起来。
-Code: mycpu and myproc
+
// Per-CPU state.
struct cpu {
struct proc *proc; // The process running on this cpu, or null.
struct context context; // swtch() here to enter scheduler().
int noff; // Depth of push_off() nesting.
int intena; // Were interrupts enabled before push_off()?
};Code: inode
++主要是在讲inode layer这一层的方法,以及给上层提供的接口。
+Overview
+
底层接口
+++
iget
iput
iget
逻辑还是跟buffer cache非常相似的,不过可以看出这个的数据结构简单许多,也不用实现LRU。
+++A struct inode pointer returned by iget() is guaranteed to be valid until the corresponding call to iput(): the inode won’t be deleted, and the memory referred to by the pointer won’t be re-used for a different inode. 【通过ref++实现。】
+不同于buffer cache的
+bget
,iget()
提供对inode的非独占访问,因此可以有许多指向同一inode的指针。文件系统代码的许多部分都依赖于iget()
的这种行为,既可以保存对inode的长期引用(如打开的文件和当前目录),也可以防止争用,同时避免操纵多个inode(如路径名查找)的代码产生死锁。-
// Find the inode with number inum on device dev
// and return the in-memory copy. Does not lock
// the inode and does not read it from disk.
static struct inode*
iget(uint dev, uint inum)
{
struct inode *ip, *empty;
acquire(&icache.lock);
// Is the inode already cached?
empty = 0;
for(ip = &icache.inode[0]; ip < &icache.inode[NINODE]; ip++){
if(ip->ref > 0 && ip->dev == dev && ip->inum == inum){
ip->ref++;
release(&icache.lock);
return ip;
}
// 由于不用实现LRU,所以只需一次循环记录即可。
if(empty == 0 && ip->ref == 0) // Remember empty slot.
empty = ip;
}
// Recycle an inode cache entry.
if(empty == 0)
panic("iget: no inodes");
ip = empty;
ip->dev = dev;
ip->inum = inum;
ip->ref = 1;
// does not read from disk
ip->valid = 0;
release(&icache.lock);
return ip;
}mycpu是通过获取当前cpuid来获取cpu结构的。当前使用的cpuid约定俗成地存在了tp寄存器里。为了让mycpu有效工作,必须确保tp寄存器始终存放的是当前cpu的hartid。
-首先是在操作系统初始化的时候要把cpuid存入tp寄存器。RISC-V规定,mhartid也即cpuid的存放点只能在machine mode被读取。因而这项工作得在
-start.c
中完成:+
// in kernel/start.c
// keep each CPU's hartid in its tp register, for cpuid().
int id = r_mhartid();
w_tp(id);
// in kernel/riscv.h
// which hart (core) is this?
static inline uint64
r_mhartid()
{
uint64 x;
asm volatile("csrr %0, mhartid" : "=r" (x) );
return x;
}iput
+++
iput()
可以写入磁盘。这意味着任何使用文件系统的系统调用都可能写入磁盘,因为系统调用可能是最后一个引用该文件的系统调用。即使像read()
这样看起来是只读的调用,也可能最终调用iput()
。这反过来意味着,即使是只读系统调用,如果它们使用文件系统,也必须在事务中进行包装。+
iput()
和崩溃之间存在一种具有挑战性的交互。iput()
不会在文件的链接计数降至零时立即截断文件,因为某些进程可能仍在内存中保留对inode的引用:进程可能仍在读取和写入该文件,因为它已成功打开该文件。但是,如果在最后一个进程关闭该文件的文件描述符之前发生崩溃,则该文件将被标记为已在磁盘上分配,但没有目录项指向它。如果不做任何处理措施的话,这块磁盘就再也用不了了。文件系统以两种方式之一处理这种情况。简单的解决方案用于恢复时:重新启动后,文件系统会扫描整个文件系统,以查找标记为已分配但没有指向它们的目录项的文件。如果存在任何此类文件,接下来可以将其释放。
+第二种解决方案不需要扫描文件系统。在此解决方案中,文件系统在磁盘(例如在超级块中)上记录链接计数降至零但引用计数不为零的文件的i-number。如果文件系统在其引用计数达到0时删除该文件,则会通过从列表中删除该inode来更新磁盘列表。重新启动时,文件系统将释放列表中的所有文件。
+Xv6没有实现这两种解决方案,这意味着inode可能被标记为已在磁盘上分配,即使它们不再使用。这意味着随着时间的推移,xv6可能会面临磁盘空间不足的风险。
+-
// Drop a reference to an in-memory inode.
// If that was the last reference, the inode cache entry can
// be recycled.【refvnt==0 可以回收】
// 注意这个回收过程无需特别处理,只需自然--refcnt就行,不用像buffer cache那么烦
// If that was the last reference and the inode has no links
// to it, free the inode (and its content) on disk.【nlinks==0 copy和本体都得扔掉】
// All calls to iput() must be inside a transaction in
// case it has to free the inode.任何需要iput的地方都需要包裹在事务内,因为它可能会释放inode
void
iput(struct inode *ip)
{
acquire(&icache.lock);
if(ip->ref == 1 && ip->valid && ip->nlink == 0){
// inode has no links and no other references: truncate and free.
// ip->ref == 1 means no other process can have ip locked,
// so this acquiresleep() won't block (or deadlock).
acquiresleep(&ip->lock);
release(&icache.lock);
// 最终调用bfree,会标记bitmap,完全释放block
itrunc(ip);
ip->type = 0;
/*iupdate:
// Copy a modified in-memory inode to disk.
// Must be called after every change to an ip->xxx field
// that lives on disk, since i-node cache is write-through.
write-through:
CPU向cache写入数据时,同时向memory(后端存储)也写一份,使cache和memory的数据保持一致。
*/
// 这里修改的type是dinode也有的字段,所以需要update一下。
// 下面的valid是dinode没有的字段,所以随便改,无需update
iupdate(ip);
ip->valid = 0;
releasesleep(&ip->lock);
acquire(&icache.lock);
}
ip->ref--;
release(&icache.lock);
}在内核态中,编译器被设置为保证不会以其他方式使用tp寄存器。因而初始化之后,内核态中每个CPU的tp寄存器就始终存放着自己的cpuid。
-但这在用户进程是不成立的。因而必须在用户进程进入陷阱的时候做一些工作。
-+
# in kernel/trampoline.S uservec
sd tp, 64(a0)
# make tp hold the current hartid, from p->trapframe->kernel_hartid
ld tp, 32(a0)上层接口
获取和释放inode
ialloc
-
// Allocate an inode on device dev.
// Mark it as allocated by giving it type type.
// Returns an unlocked but allocated and referenced inode.
struct inode*
ialloc(uint dev, short type)
{
int inum;
struct buf *bp;
struct dinode *dip;
for(inum = 1; inum < sb.ninodes; inum++){
bp = bread(dev, IBLOCK(inum, sb));
dip = (struct dinode*)bp->data + inum%IPB;
if(dip->type == 0){ // a free inode通过type判断是否free
memset(dip, 0, sizeof(*dip));// zerod
dip->type = type;
log_write(bp); // mark it allocated on the disk
brelse(bp);
return iget(dev, inum);
}
brelse(bp);
}
panic("ialloc: no inodes");
}+
struct trapframe {
/* 32 */ uint64 kernel_hartid; // saved kernel tp
/* 64 */ uint64 tp;
// ...
}inode的锁保护
前面说到,inode的设计使得有多个指针同时指向一个inode成为了可能。因而,修改使用inode的时候就要对其进行独占访问。使用
+ialloc
获取和用ifree
释放的inode必须被保护在ilock
和iunlock
区域中。ilock
+
ilock
既可以实现对inode的独占访问,同时也可以给未初始化的inode进行初始化工作。+++
iget
返回的struct inode
可能没有任何有用的内容。为了确保它保存磁盘inode的副本,代码必须调用ilock
。这将锁定inode(以便没有其他进程可以对其进行ilock
),并从磁盘读取尚未读取的inode。-
// Lock the given inode and reads the inode from disk if necessary.
void
ilock(struct inode *ip)
{
struct buf *bp;
struct dinode *dip;
if(ip == 0 || ip->ref < 1)
panic("ilock");
acquiresleep(&ip->lock);
if(ip->valid == 0){
// 通过inode索引号和superblock算出扇区号
bp = bread(ip->dev, IBLOCK(ip->inum, sb));
dip = (struct dinode*)bp->data + ip->inum%IPB;
// 填充ip
ip->type = dip->type;
ip->major = dip->major;
ip->minor = dip->minor;
ip->nlink = dip->nlink;
ip->size = dip->size;
memmove(ip->addrs, dip->addrs, sizeof(ip->addrs));
brelse(bp);
ip->valid = 1;
if(ip->type == 0)
panic("ilock: no type");
}
}必须在trampoline保存用户态中使用的tp值,以及内核态中对应的hartid。
-最后再在返回用户态的时候恢复用户态的tp值以及更新trampoline的tp值。
-+
// in kernel/trap.c usertrapret()
p->trapframe->kernel_hartid = r_tp(); // hartid for cpuid()iunlock
+++
iunlock
释放inode上的锁。将inode指针的获取与锁定分离有助于在某些情况下避免死锁,例如在目录查找期间。多个进程可以持有指向
+iget
返回的inode的C指针,但一次只能有一个进程锁定inode。-
// Unlock the given inode.
void
iunlock(struct inode *ip)
{
if(ip == 0 || !holdingsleep(&ip->lock) || ip->ref < 1)
panic("iunlock");
releasesleep(&ip->lock);
}+
# in trampoline.S userret
ld tp, 64(a0)Code: inode content
Overview
++主要讲的是inode本身存储数据的结构
+++磁盘上的inode结构体
+struct dinode
包含一个size
和一个块号数组(见图8.3),数组内罗列着存储着该inode数据的块号。前面的
+NDIRECT
个数据块被列在数组中的前NDIRECT
个元素中;这些块称为直接块(direct blocks)。接下来的NINDIRECT
个数据块不在inode中列出,而是在称为间接块(indirect block)的数据块中列出。addrs
数组中的最后一个元素给出了间接块的地址。因此,可以从inode中列出的块加载文件的前12 kB(
+NDIRECT x BSIZE
)字节,而只有在查阅间接块后才能加载下一个256 kB(NINDIRECT x BSIZE
)字节。-
// On-disk inode structure
struct dinode {
// ...
uint addrs[NDIRECT+1]; // Data block addresses
};注意,更新trampoline的tp值这一步很重要。因为如果在用户态发生的是时钟中断,就会引起yield,可能造成CPU的切换。这时候就需要在返回用户态的时候修改一下trapframe中的tp为当前CPU的tp。这样一来才能保证,在本次时钟中断结束,以及到下一次时钟中断修改CPU这一期间,trapframe中的tp寄存器以及内核态中的tp寄存器都是正确的。
-通过
-mycpu()
获取cpuid其实是非常脆弱的。因为你可能获取完cpuid,进程就被切到别的CPU去执行了,这就会有一个先检查后执行的竞态条件,具有并发安全隐患。因而,xv6要求使用mycpu()
返回值的这段代码需要关中断,这样就可以避免时钟中断造成的进程切换了。比如说像myproc()
这样:+
// Return the current struct proc *, or zero if none.
struct proc*
myproc(void) {
push_off();
struct cpu *c = mycpu();
struct proc *p = c->proc;
pop_off();
return p;
}+
bmap
++函数
+bmap
负责封装这个寻找数据块的过程,以便实现我们将很快看到的如readi
和writei
这样的更高级例程。+
bmap(struct inode *ip, uint bn)
返回inodeip
的第bn
个数据块的磁盘块号。如果ip
还没有这样的块,bmap
会分配一个。+
Bmap
使readi
和writei
很容易获取inode的数据。-
// Inode content
//
// The content (data) associated with each inode is stored
// in blocks on the disk. The first NDIRECT block numbers
// are listed in ip->addrs[]. The next NINDIRECT blocks are
// listed in block ip->addrs[NDIRECT].
// Return the disk block address of the nth block in inode ip.
// If there is no such block, bmap allocates one.
static uint
bmap(struct inode *ip, uint bn)
{
uint addr, *a;
struct buf *bp;
// 如果为direct block
if(bn < NDIRECT){
if((addr = ip->addrs[bn]) == 0)
ip->addrs[bn] = addr = balloc(ip->dev);
return addr;
}
bn -= NDIRECT;
// 如果为indirect block
if(bn < NINDIRECT){
// Load indirect block, allocating if necessary.
if((addr = ip->addrs[NDIRECT]) == 0)
ip->addrs[NDIRECT] = addr = balloc(ip->dev);
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
if((addr = a[bn]) == 0){
// 如果没有,会分配一个
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
panic("bmap: out of range");
}注意,不同于
-mycpu()
,使用myproc()
的返回值不需要进行开关中断保护。因为当前进程的指针不论处于哪个CPU都是不变的。Sleep and wakeup
前面我们已经介绍了进程隔离性的基本图像,接下来要讲xv6是如何让进程之间互动的。xv6使用的是经典的sleep and wakeup,也叫序列协调(sequence coordination)或条件同步机制(conditional synchronization mechanisms。下面,将从最基本的自旋锁实现信号量开始,来逐步讲解xv6的sleep and wakeup机制。
-自旋锁实现信号量
-
-
缺点就是自旋太久了,因而我们需要在等待的时候调用yield,直到资源生产出来之后再继续执行。
-不安全的sleep and wakeup
-Let’s imagine a pair of calls, sleep and wakeup, that work as follows:
--
+- -
-
sleep(chan)
Sleeps on the arbitrary value chan, called the wait channel. Sleep puts the calling process to sleep, releasing the CPU for other work.
-- -
-
wakeup(chan)
Wakes all processes sleeping on chan (if any), causing their sleep calls to return. If no processes are waiting on chan, wakeup does nothing.
-itrunc
+-+
itrunc
释放文件的块,将inode的size
重置为零。
Itrunc
首先释放直接块,然后释放间接块中列出的块,最后释放间接块本身。这样一来,信号量实现就可修改为这样了:
--
但是,我们可以注意到,在212-213行这里产生了一个先检查后执行的竞态条件。
--如果消费者进程执行到212-213中间,此时生产者进程已经调用结束,也就是说wakeup并没有唤醒任何消费者进程。消费者进程就会一直在sleep中没人唤醒,除非生产者进程再执行一次。这样就会造成lost wake-up 这个问题。
+readi
+-
readi
和writei
都是从检查ip->type == T_DEV
开始的。这种情况处理的是数据不在文件系统中的特殊设备;我们将在文件描述符层返回到这种情况。所以,我们可以选择把这个竞态条件也放入s->lock这个锁区域保护。
--
但是这样一来又会产生死锁问题。因而,我们可以尝试着修改sleep和wakeup的接口定义。
-sleep and wakeup
-We’ll fix the preceding scheme by changing sleep’s interface:
-The caller must pass the condition lock to sleep so it can release the lock after the calling process is marked as asleep and waiting on the sleep channel. The lock will force a concurrent V to wait until P has finished putting itself to sleep, so that the wakeup will find the sleeping consumer and wake it up. Once the consumer is awake again sleep reacquires the lock before returning.
-也即在sleep中:
-+
sleep(s,&s->lock){
// do something
release(&s->lock);
//wait until wakeup
acquire(&s->lock);
return;
}+ +
// Read data from inode.数据大小为n,从off开始,读到dst处
// Caller must hold ip->lock.
// If user_dst==1, then dst is a user virtual address;
// otherwise, dst is a kernel address.
int
readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n)
{
uint tot, m;
struct buf *bp;
if(off > ip->size || off + n < off)
return 0;
if(off + n > ip->size)
n = ip->size - off;
// 主循环处理文件的每个块,将数据从缓冲区复制到dst
for(tot=0; tot<n; tot+=m, off+=m, dst+=m){
bp = bread(ip->dev, bmap(ip, off/BSIZE));
m = min(n - tot, BSIZE - off%BSIZE);
if(either_copyout(user_dst, dst, bp->data + (off % BSIZE), m) == -1) {
brelse(bp);
tot = -1;
break;
}
brelse(bp);
}
return tot;
}writei
+ +
// Write data to inode.
// Caller must hold ip->lock.
// If user_src==1, then src is a user virtual address;
// otherwise, src is a kernel address.
int
writei(struct inode *ip, int user_src, uint64 src, uint off, uint n)
{
uint tot, m;
struct buf *bp;
if(off > ip->size || off + n < off)
return -1;
// writei会自动增长文件,除非达到文件的最大大小
if(off + n > MAXFILE*BSIZE)
return -1;
for(tot=0; tot<n; tot+=m, off+=m, src+=m){
bp = bread(ip->dev, bmap(ip, off/BSIZE));
m = min(n - tot, BSIZE - off%BSIZE);
if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
brelse(bp);
n = -1;
break;
}
log_write(bp);
brelse(bp);
}
if(n > 0){
if(off > ip->size)
// 说明扩大了文件大小,需要修改
ip->size = off;
// write the i-node back to disk even if the size didn't change
// because the loop above might have called bmap() and added a new
// block to ip->addrs[].
iupdate(ip);
}
return n;
}stati
+-函数
stati
将inode元数据复制到stat
结构体中,该结构通过stat
系统调用向用户程序公开。这样一来,信号量就可以完美实现了:
--
-
-注:严格地说,
-wakeup
只需跟在acquire
之后就足够了(也就是说,可以在release
之后调用wakeup
)【想了一下,有一说一确实,放在release前后都不影响】
+在
+defs.h
中可看到inode结构体是private的,而stat是public的。Directory layer
数据结构
+-目录的内部实现很像文件。其inode的
+type
为T_DIR
,其数据是directory entries的集合。每个entry都是一个
struct dirent
。-原始Unix内核的
+sleep
只是禁用了中断,这就足够了,因为Unix运行在单CPU系统上。因为xv6在多处理器上运行,所以它为sleep
添加了一个显式锁。也就是说这一层其实本质上是一个大小一定的map,该map自身也存放在inode中,大小为inode的大小,每个表项entry映射了目录名和文件inode。所以接下来介绍的函数我们完全可以从hashmap增删改查的角度去理解。
++ +
// Directory is a file containing a sequence of dirent structures.
struct dirent {
ushort inum;// 如果为0,说明该entry free
char name[DIRSIZ];
};+
相关函数
dirlookup
+-函数
+dirlookup
在directory中搜索具有给定名称的entry。它返回的指向enrty.inum相应的inode是非独占的【通过iget获取】,也即无锁状态。它还会把
+*poff
设置为所需的entry的字节偏移量。为什么要返回未锁定的inode?是因为调用者已锁定
+dp
,因此,如果对.
进行查找,则在返回之前尝试锁定inode将导致重新锁定dp
并产生死锁【确实】(还有更复杂的死锁场景,涉及多个进程和..
,父目录的别名。.
不是唯一的问题。)所以锁定交给caller来做。caller可以解锁
dp
,然后锁定该函数返回的ip
,确保它一次只持有一个锁。Code: Sleep and wakeup
+
// Atomically release lock and sleep on chan.
// Reacquires lock when awakened.
void
sleep(void *chan, struct spinlock *lk)
{
struct proc *p = myproc();
// Must acquire p->lock in order to
// change p->state and then call sched.
// Once we hold p->lock, we can be
// guaranteed that we won't miss any wakeup
// (wakeup locks p->lock),
// so it's okay to release lk.
if(lk != &p->lock){ //DOC: sleeplock0
// 获取进程锁,释放外部锁
// 此进程锁将在scheduler线程中释放
acquire(&p->lock); //DOC: sleeplock1
release(lk);
}
// Go to sleep.
p->chan = chan;
p->state = SLEEPING;
sched();
// 到这里来,说明已经被wakeup且被调度了
// Tidy up.
p->chan = 0;
// Reacquire original lock.
if(lk != &p->lock){
//释放进程锁,获取外部锁
// 此进程锁是在scheduler中获取到的
release(&p->lock);
acquire(lk);
}
}-
// Look for a directory entry in a directory.
// If found, set *poff to byte offset of entry.
struct inode*
dirlookup(struct inode *dp, char *name, uint *poff)
{
uint off, inum;
struct dirent de;
if(dp->type != T_DIR)
panic("dirlookup not DIR");
// new level of abstraction,可以把directory的inode看作一个表文件,每个表项都是一个entry
for(off = 0; off < dp->size; off += sizeof(de)){
// 从directory中获取entry,也即从inode中获取数据
if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlookup read");
// free
if(de.inum == 0)
continue;
if(namecmp(name, de.name) == 0){
// entry matches path element
if(poff)
*poff = off;
inum = de.inum;
return iget(dp->dev, inum);
}
}
return 0;
}注意,如果lk为p->lock,那么lk依然会在scheduler线程中被暂时释放。
-+
// Wake up all processes sleeping on chan.
// Must be called without any p->lock.
void
wakeup(void *chan)
{
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++) {
acquire(&p->lock);
if(p->state == SLEEPING && p->chan == chan) {
p->state = RUNNABLE;
}
release(&p->lock);
}
}dirlink
-
// Write a new directory entry (name, inum) into the directory dp.
int
dirlink(struct inode *dp, char *name, uint inum)
{
int off;
struct dirent de;
struct inode *ip;
// Check that name is not present.
if((ip = dirlookup(dp, name, 0)) != 0){
iput(ip);
return -1;
}
// Look for an empty dirent.
for(off = 0; off < dp->size; off += sizeof(de)){
if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlink read");
if(de.inum == 0)
break;
}
// 如果没找到空闲的则调用writei自动增长inode,添加新表项
strncpy(de.name, name, DIRSIZ);
de.inum = inum;
if(writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlink");
return 0;
}可以注意到,关于
-chan
这一变量的取值是非常任意的,仅需取一个约定俗成的值就OK。这里取为了信号量的地址,同时满足了逻辑需求和语义需求。-Callers of sleep and wakeup can use any mutually convenient number as the channel. Xv6 often uses the address of a kernel data structure involved in the waiting.
+Pathname layer
+-Path name lookup involves a succession of calls to dirlookup, one for each path component.
这里也解释了为什么需要while循环
--有时,多个进程在同一个通道上睡眠;例如,多个进程读取同一个管道。一个单独的
+wakeup
调用就能把他们全部唤醒。其中一个将首先运行并获取与sleep
一同调用的锁,并且(在管道例子中)读取在管道中等待的任何数据。尽管被唤醒,其他进程将发现没有要读取的数据。从他们的角度来看,醒来是“虚假的”,他们必须再次睡眠。因此,在检查条件的循环中总是调用sleep
。namei和nameiparent
+-Namei (kernel/fs.c:661) evaluates path and returns the corresponding inode.
+函数
nameiparent
是一个变体:它在最后一个元素之前停止,返回父目录的inode并将最后一个元素复制到name
中。两者都调用通用函数namex
来完成实际工作。Code: Pipes
pipes很显然就是生产者消费者模式的一个例证。
-+
struct pipe {
struct spinlock lock;
char data[PIPESIZE];
uint nread; // number of bytes read
uint nwrite; // number of bytes written
int readopen; // read fd is still open
int writeopen; // write fd is still open
};-
struct inode*
namei(char *path)
{
char name[DIRSIZ];
return namex(path, 0, name);
}+
int
piperead(struct pipe *pi, uint64 addr, int n)
{
int i;
struct proc *pr = myproc();
char ch;
acquire(&pi->lock);
while(pi->nread == pi->nwrite && pi->writeopen){ //DOC: pipe-empty并且依然有进程在写
if(pr->killed){
release(&pi->lock);
return -1;
}
// 等待直到pipe不为空
sleep(&pi->nread, &pi->lock); //DOC: piperead-sleep
}
for(i = 0; i < n; i++){ //DOC: piperead-copy
if(pi->nread == pi->nwrite)
break;
ch = pi->data[pi->nread++ % PIPESIZE];
if(copyout(pr->pagetable, addr + i, &ch, 1) == -1)
break;
}
// 唤醒写入管道的进程
wakeup(&pi->nwrite); //DOC: piperead-wakeup
release(&pi->lock);
return i;
}-
struct inode*
nameiparent(char *path, char *name)
{
return namex(path, 1, name);
}+
int
pipewrite(struct pipe *pi, uint64 addr, int n)
{
int i;
char ch;
struct proc *pr = myproc();
acquire(&pi->lock);
for(i = 0; i < n; i++){
while(pi->nwrite == pi->nread + PIPESIZE){ //DOC: pipewrite-full管道满则阻塞
if(pi->readopen == 0 || pr->killed){
release(&pi->lock);
return -1;
}
// 唤醒读取管道的进程
wakeup(&pi->nread);
sleep(&pi->nwrite, &pi->lock);
}
if(copyin(pr->pagetable, &ch, addr + i, 1) == -1)
break;
pi->data[pi->nwrite++ % PIPESIZE] = ch;
}
wakeup(&pi->nread);
release(&pi->lock);
return i;
}namex
+++
Namex
首先决定路径解析的开始位置。如果路径以“ / ”开始,则从根目录开始解析;否则,从当前目录开始。
+然后,它使用
+skipelem
依次考察路径的每个元素。循环的每次迭代都必须在当前索引结点ip
中查找name
。迭代首先给
+ip
上锁并检查它是否是一个目录。如果不是,则查找失败。如果caller是
+nameiparent
,并且这是最后一个路径元素,则根据nameiparent
的定义,循环会提前停止;最后一个路径元素已经复制到name
中【在上一轮循坏中做了这件事】,因此namex
只需返回解锁的ip
。最后,循环将使用
+dirlookup
查找路径元素,并通过设置ip = next
为下一次迭代做准备。当循环用完路径元素时,它返回ip
。注:
++
+- 在每次迭代中锁定
+ip
是必要的,不是因为ip->type
可以被更改,而是因为在ilock
运行之前,ip->type
不能保证已从磁盘加载,所以得用到ilock保证一定会被加载的这个性质。-
// Look up and return the inode for a path name.
// If parent != 0, return the inode for the parent and copy the final
// path element into name, which must have room for DIRSIZ bytes.
// Must be called inside a transaction since it calls iput().
static struct inode*
namex(char *path, int nameiparent, char *name)
{
struct inode *ip, *next;
if(*path == '/')
ip = iget(ROOTDEV, ROOTINO);
else
ip = idup(myproc()->cwd);
// 使用skipelem依次考察路径的每个元素
while((path = skipelem(path, name)) != 0){
ilock(ip);
if(ip->type != T_DIR){
iunlockput(ip);
return 0;
}
if(nameiparent && *path == '\0'){
// Stop one level early.
iunlock(ip);
return ip;
}
if((next = dirlookup(ip, name, 0)) == 0){
iunlockput(ip);
return 0;
}
iunlockput(ip);
ip = next;
}
if(nameiparent){
iput(ip);
return 0;
}
return ip;
}一个非常有意思且巧妙的点,就是读写管道等待在不同的chan上,这与上面信号量的例子是不一样的。想想也确实,如果使用同一个管道的话,当唤醒的时候,就会把不论是读还是写的全部进程都唤醒过来,这对性能显然损失较大。
--The pipe code uses separate sleep channels for reader and writer (pi->nread and pi->nwrite); this might make the system more effificient in the unlikely event that there are lots of readers and writers waiting for the same pipe.
++
namex
过程可能需要很长时间才能完成:它可能涉及多个磁盘操作来读取路径名中所遍历目录的索引节点和目录块(如果它们不在buffer cache中)。Xv6 is carefully designed,如果一个内核线程对
+namex
的调用在磁盘I/O上阻塞,另一个查找不同路径名的内核线程可以同时进行。Namex
locks each directory in the path separately so that lookups in different directories can proceed in parallel.锁细粒度化This concurrency introduces some challenges. For example, while one kernel thread is looking up a pathname another kernel thread may be changing the directory tree by unlinking a directory. A potential risk is that a lookup may be searching a directory that has been deleted by another kernel thread and its blocks have been re-used for another directory or file.一个潜在的风险是,查找可能正在搜索已被另一个内核线程删除且其块已被重新用于另一个目录或文件的目录。
+Xv6避免了这种竞争,也就是说,你查到的inode保证暂时不会被释放,里面的内容还是真的,而不会被重新利用从而导致里面的内容变样。
+例如,在
+namex
中执行dirlookup
时,lookup线程持有目录上的锁,dirlookup
返回使用iget
获得的inode。Iget
增加索引节点的引用计数。只有在从dirlookup
接收inode之后,namex
才会释放目录上的锁。现在,另一个线程可以从目录中取消inode的链接,但是xv6还不会删除inode,因为inode的引用计数仍然大于零。另一个风险是死锁。例如,查找“
.
”时,next
指向与ip
相同的inode【确实】。在释放ip
上的锁之前锁定next
将导致死锁【为什么???难道不是会由于在acquire时已经持有锁,从而爆panic("acquire")
吗?】。为了避免这种死锁,namex
在获得下一个目录的锁之前解锁该目录。这里我们再次看到为什么iget
和ilock
之间的分离很重要。Code: Wait, exit, and kill
exit和wait
--
Sleep
和wakeup
可用于多种等待。第一章介绍的一个有趣的例子是子进程exit
和父进程wait
之间的交互。xv6记录子进程终止直到
-wait
观察到它的方式是让exit
将调用方置于ZOMBIE
状态,在那里它一直保持到父进程的wait
注意到它,将子进程的状态更改为UNUSED
,复制子进程的exit
状态码,释放子进程,并将子进程ID返回给父进程。如果父进程在子进程之前退出,则父进程将子进程交给
+init
进程,init
进程将永久调用wait
;因此,每个子进程退出后都有一个父进程进行清理。File descriptor layer
+-Unix的一个很酷的方面是,Unix中的大多数资源都表示为文件,包括控制台、管道等设备,当然还有真实文件。文件描述符层是实现这种一致性的层。
又是一个生产者消费者模式。只不过此时的chan是父进程,资源是僵尸子进程【草】。由于涉及到进程间的调度切换,因而实现稍微复杂了点。
-为什么需要涉及到进程间的调度呢?子进程设置完僵尸状态后,直接通过函数ret不行吗?答案是不行,因为ret的话就会去到不知道哪的地方【大概率会变成scause=2的情况】,所以这里子进程想要退出,就得做几件事,一是依靠父进程,让父进程杀死子进程,二是把自己设置为一个特殊的状态,使得自己不会被调度从而执行ret指令出错,三是尽快让父进程杀死自己越快越好。综合上述三个原因,exit最终在调度方面的实现方式,就变成了,子进程设置自己为ZOMBIE态->启用调度->父进程杀死ZOMBIE态的子进程。这期间不变性条件的防护,就得依赖于锁,以及sleep和wakeup了。
-+
void
exit(int status)
{
struct proc *p = myproc();
// ...
// we need the parent's lock in order to wake it up from wait().
// the parent-then-child rule says we have to lock it first.
// 整个xv6都必须遵守相同的顺序(父级,然后是子级)不论是锁定还是释放,都是先父再子
acquire(&original_parent->lock);
acquire(&p->lock);
// Give any children to init.
// 把自己的所有孩子都托付给init进程
// init进程就是在操作系统启动时
reparent(p);
// Parent might be sleeping in wait().
// 唤醒wait中的父进程
// 这里看上去很诡异,明明子进程状态还未完全,怎么就唤醒父亲了呢?但其实很安全。
// 此时子进程仍持有父进程的锁,如果有别的CPU中断进入scheduler线程,到父进程那时会卡在aquire
// 直到子进程完成后续工作后父进程才能被真正唤醒执行
wakeup1(original_parent);
p->xstate = status;
// 设为ZOMBIE态
p->state = ZOMBIE;
// 完成后续工作,解除父进程的锁
release(&original_parent->lock);
// Jump into the scheduler, never to return.
// 子进程会在父进程中被释放,所以永远不会回来
sched();
panic("zombie exit");
}数据结构
++Xv6为每个进程提供了自己的打开文件表或文件描述符。每个打开的文件都由一个
+struct file
表示,它是inode或管道的封装,加上一个I/O偏移量。每次调用
+open
都会创建一个新的打开文件(一个新的struct file
):如果多个进程独立地打开同一个文件,那么不同的实例将具有不同的I/O偏移量。另一方面,单个打开的文件(同一个
+struct file
)可以多次出现在一个进程的文件表中,也可以出现在多个进程的文件表中。如果一个进程使用open
打开文件,然后使用dup
创建别名,或使用fork
与子进程共享,就会发生这种情况。-
struct file {
enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
int ref; // reference count
char readable;
char writable;
struct pipe *pipe; // FD_PIPE
struct inode *ip; // FD_INODE and FD_DEVICE
uint off; // FD_INODE
short major; // FD_DEVICE
};+
int
wait(uint64 addr)
{
struct proc *np;
int havekids, pid;
struct proc *p = myproc();
// hold p->lock for the whole time to avoid lost
// wakeups from a child's exit().
acquire(&p->lock);
for(;;){
// Scan through table looking for exited children.
havekids = 0;
for(np = proc; np < &proc[NPROC]; np++){
// this code uses np->parent without holding np->lock.
// acquiring the lock first would cause a deadlock,
// since np might be an ancestor, and we already hold p->lock.
// 下面的第一点其实一句话就可以搞定:
// 【它违反了先获取父亲锁,再获取子锁的xv6代码规定】
// 1.要是在这句话之前acquire的话,acquire到你爸,你爸这时候也刚好执行到这句话
// 那么就会造成你在自旋【此时你爸在wait一开始就得到了锁】,
// 你爸也在自旋【你也在wait一开始得到了锁】,这样就造成了死锁
// 2.并且由于np->parent只有parent才能改,所以数据是否过时是没关系的
// 因为如果不是你儿子,数据过时与否都知道不是你儿子
// 如果是你儿子,那数据压根就不会过时
if(np->parent == p){
// np->parent can't change between the check and the acquire()
// because only the parent changes it, and we're the parent.
acquire(&np->lock);
havekids = 1;
if(np->state == ZOMBIE){
// Found one.
pid = np->pid;
// 传递返回参数
if(addr != 0 && copyout(p->pagetable, addr, (char *)&np->xstate,
sizeof(np->xstate)) < 0) {
release(&np->lock);
release(&p->lock);
return -1;
}
freeproc(np);
release(&np->lock);
release(&p->lock);
return pid;
}
release(&np->lock);
}
}
// No point waiting if we don't have any children.
if(!havekids || p->killed){
release(&p->lock);
return -1;
}
// Wait for a child to exit.
// 暂时释放p锁,等待子进程获取退出
sleep(p, &p->lock); //DOC: wait-sleep
}
}ftable
++所有在系统中打开的文件都会被放入global file table
+ftable
中。+
ftable
具有分配文件(filealloc
)、创建重复引用(filedup
)、释放引用(fileclose
)以及读取和写入数据(fileread
和filewrite
)的函数。前三个都很常规,跟之前的xxalloc、xxfree的思路是一样的。
+函数
+filestat
、fileread
和filewrite
实现对文件的stat
、read
和write
操作。filealloc
-
// Allocate a file structure.
struct file*
filealloc(void)
{
struct file *f;
acquire(&ftable.lock);
for(f = ftable.file; f < ftable.file + NFILE; f++){
if(f->ref == 0){
f->ref = 1;
release(&ftable.lock);
return f;
}
}
release(&ftable.lock);
return 0;
}其中值得注意的几个点:
--
-- -
-
wait
中的sleep
中释放的条件锁是等待进程的p->lock
,这是上面提到的特例。- -
exit会将自己的所有子进程交付给一直在等待着的init进程:
-+
for(;;){
printf("init: starting sh\n");
pid = fork();
// ...
for(;;){
// this call to wait() returns if the shell exits,
// or if a parentless process exits.
wpid = wait((int *) 0);
if(wpid == pid){
// the shell exited; restart it.
break;
} else if(wpid < 0){
printf("init: wait returned an error\n");
exit(1);
} else {
// 这里!!
// it was a parentless process; do nothing.
}
}
}filedup
-
// Increment ref count for file f.
struct file*
filedup(struct file *f)
{
acquire(&ftable.lock);
if(f->ref < 1)
panic("filedup");
f->ref++;
release(&ftable.lock);
return f;
}如果子进程退出,就会通过init的wait释放它们。然后init释放完它们后进入第三个if分支,继续进行循环。
-- -
wakeup1
---Exit calls a specialized wakeup function, wakeup1, that wakes up only the parent, and only if it is sleeping in wait.
-
// Wake up p if it is sleeping in wait(); used by exit().
// Caller must hold p->lock.
static void
wakeup1(struct proc *p)
{
if(!holding(&p->lock))
panic("wakeup1");
if(p->chan == p && p->state == SLEEPING) {
p->state = RUNNABLE;
}
}kill
kill其实做得很温和。它只是会把想鲨的进程的p->killed设置为1,然后如果该进程sleeping,则唤醒它。最后的死亡以及销毁由进程自己来做。
-+
// Kill the process with the given pid.
// The victim won't exit until it tries to go
// to kernel space (see usertrap() in trap.c).
int
kill(int pid)
{
struct proc *p;
for(p = proc; p < &proc[NPROC]; p++){
acquire(&p->lock);
if(p->pid == pid){
p->killed = 1;
if(p->state == SLEEPING){
// Wake process from sleep().
p->state = RUNNABLE;
}
release(&p->lock);
return 0;
}
release(&p->lock);
}
return -1;
}
// in trap.c usertrap()
if(p->killed)
exit(-1);fileclose
-
// Close file f. (Decrement ref count, close when reaches 0.)
void
fileclose(struct file *f)
{
struct file ff;
acquire(&ftable.lock);
if(f->ref < 1)
panic("fileclose");
if(--f->ref > 0){
release(&ftable.lock);
return;
}
ff = *f;
f->ref = 0;
f->type = FD_NONE;
release(&ftable.lock);
if(ff.type == FD_PIPE){
pipeclose(ff.pipe, ff.writable);
} else if(ff.type == FD_INODE || ff.type == FD_DEVICE){
begin_op();
iput(ff.ip);
end_op();
}
}可能这里有一个疑问:调用完exit后,进程会变成ZOMBIE态。谁最终把它释放了呢?其实答案很简单,只有两种:init进程或者是创建它的父进程。
-如果创建它的父进程处于wait中,那么是由父进程把它销毁的,这没什么好说的。但如果创建它的父进程不在wait呢?那么父进程最后也是会调用exit的。父进程调用完exit后,会将其所有子进程过继给init进程。所以,ZOMBIE进程最终还是会迟早被init进程杀死的。
-由这里,可以窥见xv6进程管理的进一步的冰山一角:
-init进程是所有进程的根系进程。它一直处于wait的死循环中,因而可以将需要被杀死的进程杀死。
-可见,wait和exit,实际上就构筑了进程的生命周期的最后一环。
-这种巧妙地将进程生命周期这个大事完全托付给了wait和exit这两个函数的这种结构,实在是非常精妙,太牛了吧。
--一些XV6的
+sleep
循环不检查p->killed
,因为代码在应该是原子操作的多步系统调用的中间。virtio驱动程序(*kernel/virtio_disk.c*:242)就是一个例子:它不检查p->killed
,因为一个磁盘操作可能是文件系统保持正确状态所需的一组写入操作之一。等待磁盘I/O时被杀死的进程将不会退出,直到它完成当前系统调用并且usertrap
看到killed
标志filestat
+-Filestat只允许在inode上操作并且调用了
stati
-Xv6对
+kill
的支持并不完全令人满意:有一些sleep
循环可能应该检查p->killed
。一个相关的问题是,即使对于检查p->killed
的sleep
循环,sleep
和kill
之间也存在竞争;后者可能会设置p->killed
,并试图在受害者的循环检查p->killed
之后但在调用sleep
之前尝试唤醒受害者。如果出现此问题,受害者将不会注意到p->killed
,直到其等待的条件发生。这可能比正常情况要晚一点(例如,当virtio驱动程序返回受害者正在等待的磁盘块时)或永远不会发生(例如,如果受害者正在等待来自控制台的输入,但用户没有键入任何输入)。+ +
// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
struct proc *p = myproc();
struct stat st;
// 仅允许文件/设备执行
if(f->type == FD_INODE || f->type == FD_DEVICE){
ilock(f->ip);
stati(f->ip, &st);
iunlock(f->ip);
if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
return -1;
return 0;
}
return -1;
}fileread
+ +
// Read from file f.
// addr is a user virtual address.
int
fileread(struct file *f, uint64 addr, int n)
{
int r = 0;
// 首先检查是否可读
if(f->readable == 0)
return -1;
if(f->type == FD_PIPE){
r = piperead(f->pipe, addr, n);
} else if(f->type == FD_DEVICE){
if(f->major < 0 || f->major >= NDEV || !devsw[f->major].read)
return -1;
r = devsw[f->major].read(1, addr, n);
} else if(f->type == FD_INODE){
ilock(f->ip);
if((r = readi(f->ip, 1, addr, f->off, n)) > 0)
// 移动文件指针偏移量
f->off += r;
iunlock(f->ip);
} else {
panic("fileread");
}
return r;
}Code: System calls
+-通过使用底层提供的函数,大多数系统调用的实现都很简单(请参阅***kernel/sysfile.c***)。有几个调用值得仔细看看。
+以下介绍的函数都在
kernel/sysfile.c
中。是的,所以这个kill的实现其实是相当玄学的。
-Real world
-xv6调度器实现了一个简单的调度策略:它依次运行每个进程。这一策略被称为轮询调度(round robin)。真实的操作系统实施更复杂的策略,例如,允许进程具有优先级。
+sys_link
这个函数的功能是给文件old加上一个链接,这个链接存在于文件new的父目录。感觉也就相当于把文件从old复制到new处了。具体实现逻辑就是要给该文件所在目录添加一个entry,name=新名字,inode=该文件的inode。
++ +
// Create the path new as a link to the same inode as old.
uint64
sys_link(void)
{
char name[DIRSIZ], new[MAXPATH], old[MAXPATH];
struct inode *dp, *ip;
if(argstr(0, old, MAXPATH) < 0 || argstr(1, new, MAXPATH) < 0)
return -1;
// 首先先增加nlink
begin_op();
// 通过path找到ip结点
if((ip = namei(old)) == 0){
end_op();
return -1;
}
ilock(ip);
// directory不能被link
if(ip->type == T_DIR){
iunlockput(ip);
end_op();
return -1;
}
ip->nlink++;
// 修改一次字段就需要update一次
iupdate(ip);
iunlock(ip);
// 然后再在目录中登记新的entry
// 找到new的parent,也即new所在目录
if((dp = nameiparent(new, name)) == 0)
goto bad;
ilock(dp);
// 在目录中添加一个entry,名字为给定的新名字,inode依旧为原来的inode
// new的父目录必须存在并且与现有inode位于同一设备上
if(dp->dev != ip->dev || dirlink(dp, name, ip->inum) < 0){
iunlockput(dp);
goto bad;
}
iunlockput(dp);
iput(ip);
end_op();
return 0;
bad:
ilock(ip);
ip->nlink--;
iupdate(ip);
iunlockput(ip);
end_op();
return -1;
}create
+-它是三个文件创建系统调用的泛化:带有
O_CREATE
标志的open
生成一个新的普通文件,mkdir
生成一个新目录,mkdev
生成一个新的设备文件。我记得linux0.11用的是时间片轮转+优先级队列完美融合的方法,是真的很牛逼
--复杂的策略可能会导致意外的交互,例如优先级反转(priority inversion)和航队(convoys)。当低优先级进程和高优先级进程共享一个锁时,可能会发生优先级反转,当低优先级进程持有该锁时,可能会阻止高优先级进程前进。当许多高优先级进程正在等待一个获得共享锁的低优先级进程时,可能会形成一个长的等待进程航队;一旦航队形成,它可以持续很长时间。为了避免此类问题,在复杂的调度器中需要额外的机制。
+创建一个新的inode结点,结点名包含在
+path
内。返回一个锁定的inode。由于使用了
+iupdate
等,所以该函数只能在事务中被调用。+ +
static struct inode*
create(char *path, short type, short major, short minor)
{
struct inode *ip, *dp;
char name[DIRSIZ];
// 获取结点父目录
if((dp = nameiparent(path, name)) == 0)
return 0;
ilock(dp);
if((ip = dirlookup(dp, name, 0)) != 0){
// 说明文件已存在
iunlockput(dp);
ilock(ip);
if(type == T_FILE && (ip->type == T_FILE || ip->type == T_DEVICE))
// 说明此时caller为open(type == T_FILE),open调用create只能是用于创建文件
return ip;
iunlockput(ip);
return 0;
}
if((ip = ialloc(dp->dev, type)) == 0)
panic("create: ialloc");
ilock(ip);
ip->major = major;
ip->minor = minor;
ip->nlink = 1;
iupdate(ip);
if(type == T_DIR){ // Create . and .. entries.
dp->nlink++; // for ".."
iupdate(dp);
// No ip->nlink++ for ".": avoid cyclic ref count.
// 所以其实.和..本质上是link
if(dirlink(ip, ".", ip->inum) < 0 || dirlink(ip, "..", dp->inum) < 0)
panic("create dots");
}
if(dirlink(dp, name, ip->inum) < 0)
panic("create: dirlink");
iunlockput(dp);
return ip;
}sys_mkdir
+ +
uint64
sys_mkdir(void)
{
char path[MAXPATH];
struct inode *ip;
begin_op();
if(argstr(0, path, MAXPATH) < 0 || (ip = create(path, T_DIR, 0, 0)) == 0){
end_op();
return -1;
}
iunlockput(ip);
end_op();
return 0;
}sys_open
+-
Sys_open
是最复杂的,因为创建一个新文件只是它能做的一小部分。-在
+wakeup
中扫描整个进程列表以查找具有匹配chan
的进程效率低下。一个更好的解决方案是用一个数据结构替换sleep
和wakeup
中的chan
,该数据结构包含在该结构上休眠的进程列表,例如Linux的等待队列。+ +
uint64
sys_open(void)
{
char path[MAXPATH];
int fd, omode;
struct file *f;
struct inode *ip;
int n;
if((n = argstr(0, path, MAXPATH)) < 0 || argint(1, &omode) < 0)
return -1;
begin_op();
if(omode & O_CREATE){
ip = create(path, T_FILE, 0, 0);
// 创建失败
if(ip == 0){
end_op();
return -1;
}
} else {
// 文件不存在
if((ip = namei(path)) == 0){
end_op();
return -1;
}
// Create返回一个锁定的inode,但namei不锁定,因此sys_open必须锁定inode本身。
ilock(ip);
// 非文件,为目录并且非只读
// 所以说想要open一个目录的话只能以只读模式打开
if(ip->type == T_DIR && omode != O_RDONLY){
iunlockput(ip);
end_op();
return -1;
}
}
if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
iunlockput(ip);
end_op();
return -1;
}
// 获取file结构体和文件描述符。
if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
if(f)
fileclose(f);
iunlockput(ip);
end_op();
return -1;
}
// 没有其他进程可以访问部分初始化的文件,因为它仅位于当前进程的表中,因而这里可以不用上锁
if(ip->type == T_DEVICE){
f->type = FD_DEVICE;
f->major = ip->major;
} else {
f->type = FD_INODE;
f->off = 0;
}
f->ip = ip;
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
// 如果使用了这个标志,调用 open 函数打开文件的时候会将文件原本的内容全部丢弃,文件大小变为 0。
if((omode & O_TRUNC) && ip->type == T_FILE){
itrunc(ip);
}
iunlock(ip);
end_op();
return fd;
}sys_pipe
+ +
uint64
sys_pipe(void)
{
uint64 fdarray; // user pointer to array of two integers用来接收pipe两端的文件描述符
struct file *rf, *wf;
int fd0, fd1;
struct proc *p = myproc();
if(argaddr(0, &fdarray) < 0)
return -1;
if(pipealloc(&rf, &wf) < 0)
return -1;
fd0 = -1;
if((fd0 = fdalloc(rf)) < 0 || (fd1 = fdalloc(wf)) < 0){
if(fd0 >= 0)
p->ofile[fd0] = 0;
fileclose(rf);
fileclose(wf);
return -1;
}
if(copyout(p->pagetable, fdarray, (char*)&fd0, sizeof(fd0)) < 0 ||
copyout(p->pagetable, fdarray+sizeof(fd0), (char *)&fd1, sizeof(fd1)) < 0){
p->ofile[fd0] = 0;
p->ofile[fd1] = 0;
fileclose(rf);
fileclose(wf);
return -1;
}
return 0;
}Real world
+-实际操作系统中的buffer cache比xv6复杂得多,但它有两个相同的用途:缓存和同步对磁盘的访问。
+与UNIX V6一样,Xv6的buffer cache使用简单的最近最少使用(LRU)替换策略;有许多更复杂的策略可以实现,每种策略都适用于某些工作场景,而不适用于其他工作场景。更高效的LRU缓存将消除链表,而改为使用哈希表进行查找,并使用堆进行LRU替换【跟我们在lock中实现的一样,再多个堆优化】。现代buffer cache通常与虚拟内存系统集成,以支持内存映射文件。
+Xv6的日志系统效率低下。提交不能与文件系统调用同时发生。系统记录整个块,即使一个块中只有几个字节被更改。它执行同步日志写入,每次写入一个块,每个块可能需要整个磁盘旋转时间。真正的日志系统解决了所有这些问题。
+文件系统布局中最低效的部分是目录,它要求在每次查找期间对所有磁盘块进行线性扫描【确实】。当目录只有几个磁盘块时,这是合理的,但对于包含许多文件的目录来说,开销巨大。Microsoft Windows的NTFS、Mac OS X的HFS和Solaris的ZFS(仅举几例)将目录实现为磁盘上块的平衡树。这很复杂,但可以保证目录查找在对数时间内完成(即时间复杂度为O(logn))。
+Xv6对于磁盘故障的解决很初级:如果磁盘操作失败,Xv6就会调用
+panic
。这是否合理取决于硬件:如果操作系统位于使用冗余屏蔽磁盘故障的特殊硬件之上,那么操作系统可能很少看到故障,因此panic
是可以的。另一方面,使用普通磁盘的操作系统应该预料到会出现故障,并能更优雅地处理它们,这样一个文件中的块丢失不会影响文件系统其余部分的使用。Xv6要求文件系统安装在单个磁盘设备上,且大小不变。随着大型数据库和多媒体文件对存储的要求越来越高,操作系统正在开发各种方法来消除“每个文件系统一个磁盘”的瓶颈。基本方法是将多个物理磁盘组合成一个逻辑磁盘。RAID等硬件解决方案仍然是最流行的,但当前的趋势是在软件中尽可能多地实现这种逻辑。这些软件实现通常允许通过动态添加或删除磁盘来扩展或缩小逻辑设备等丰富功能。当然,一个能够动态增长或收缩的存储层需要一个能够做到这一点的文件系统:xv6使用的固定大小的inode块阵列在这样的环境中无法正常工作。将磁盘管理与文件系统分离可能是最干净的设计,但两者之间复杂的接口导致了一些系统(如Sun的ZFS)将它们结合起来。
+Xv6的文件系统缺少现代文件系统的许多其他功能;例如,它缺乏对快照和增量备份的支持。
+现代Unix系统允许使用与磁盘存储相同的系统调用访问多种资源:命名管道、网络连接、远程访问的网络文件系统以及监视和控制接口,如
+/proc
。不同于xv6中fileread
和filewrite
的if
语句,这些系统通常为每个打开的文件提供一个函数指针表【确实有印象】,每个操作一个,并通过函数指针来援引inode的调用实现。网络文件系统和用户级文件系统提供了将这些调用转换为网络RPC并在返回之前等待响应的函数。(注:Linux 内核提供了一种通过
/proc
文件系统,在运行时访问内核内部数据结构、改变内核设置的机制。proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。)是的,linux的那个wakeup真的很牛,我现在都还记得当初学到那的时候的震撼。
---
wakeup
的实现会唤醒在特定通道上等待的所有进程,可能有许多进程在等待该特定通道。操作系统将安排所有这些进程,它们将竞相检查睡眠条件。进程的这种行为有时被称为惊群效应(thundering herd),最好避免。大多数条件变量都有两个用于唤醒的原语:
+signal
用于唤醒一个进程;broadcast
用于唤醒所有等待进程。Lab: file system
+-In this lab you will add large files【大文件支持】 and symbolic links【软链接】 to the xv6 file system.
+不过做完这个实验,给我的一种感觉就是磁盘管理和内存管理真的有很多相似之处,不过也许它们所代表的思想也很普遍。
-一个实际的操作系统将在固定时间内使用空闲列表找到自由的
+proc
结构体,而不是allocproc
中的线性时间搜索;xv6使用线性扫描是为了简单起见。Large files
实验内容
Overview
+-In this assignment you’ll increase the maximum size of an xv6 file.
+Currently xv6 files are limited to 268 blocks, or 268*BSIZE bytes (BSIZE is 1024 in xv6). This limit comes from the fact that an xv6 inode contains 12 “direct” block numbers and one “singly-indirect” block number, which refers to a block that holds up to 256 more block numbers, for a total of 12+256=268 blocks.
+You’ll change the xv6 file system code to support a “doubly-indirect” block in each inode, containing 256 addresses of singly-indirect blocks, each of which can contain up to 256 addresses of data blocks. The result will be that a file will be able to consist of up to 65803 blocks, or 256*256+256+11 blocks (11 instead of 12, because we will sacrifice one of the direct block numbers for the double-indirect block).
Lab: Multithreading
-You will implement switching between threads in a user-level threads package, use multiple threads to speed up a program, and implement a barrier.
+Preliminaries
+-If at any point during the lab you find yourself having to rebuild the file system from scratch, you can run
make clean
which forces make to rebuild fs.img.这个introduction看起来还是非常激动人心的,很早就想了解到底线程是怎么实现的了。不过做完发现思想还是很简单的,就是只用切换上下文和栈就行。可以看看提供给的代码。
-Uthread: switching between threads
-In this exercise you will design the context switch mechanism for a user-level threading system, and then implement it.
-To get you started, your xv6 has two files
-user/uthread.c
anduser/uthread_switch.S
, and a rule in the Makefile to build a uthread program.-
uthread.c
contains most of a user-level threading package, and code for three simple test threads. The threading package is missing some of the code to create a thread and to switch between threads.You will need to add code to
-thread_create()
andthread_schedule()
inuser/uthread.c
, andthread_switch
inuser/uthread_switch.S
.One goal is ensure that when
-thread_schedule()
runs a given thread for the first time, the thread executes the function passed tothread_create()
, on its own stack.Another goal is to ensure that
-thread_switch
saves the registers of the thread being switched away from, restores the registers of the thread being switched to, and returns to the point in the latter thread’s instructions where it last left off. You will have to decide where to save/restore registers; modifyingstruct thread
to hold registers is a good plan.You’ll need to add a call to
+thread_switch
inthread_schedule
; you can pass whatever arguments you need tothread_switch
, but the intent is to switch from threadt
tonext_thread
.What to Look At
意思就是要我们去看一眼fs.h,bmap,以及了解一下逻辑地址bn如何转化为blockno。这个我是知道的。
+Your Job
+-Modify
+bmap()
so that it implements a doubly-indirect block, in addition to direct blocks and a singly-indirect block.You’ll have to have only 11 direct blocks, rather than 12, to make room for your new doubly-indirect block; you’re not allowed to change the size of an on-disk inode.
+The first 11 elements of
ip->addrs[]
should be direct blocks; the 12th should be a singly-indirect block (just like the current one); the 13th should be your new doubly-indirect block. You are done with this exercise whenbigfile
writes 65803 blocks andusertests
runs successfully.实现的是用户级线程,其栈保存在对应父进程的地址空间中。
-感想
思路
看了一遍它这里面写的题目还是有点抽象的,需要结合着给的代码看,那样就清晰多了。
-首先,要补全的地方有这几个:
-+
// 1. in thread_schedule()
if (current_thread != next_thread) { /* switch threads? */
next_thread->state = RUNNING;
t = current_thread;
current_thread = next_thread;
/* YOUR CODE HERE
* Invoke thread_switch to switch from t to next_thread:
* thread_switch(??, ??);
*/
} else
next_thread = 0;
// 2. in thread_create()
void
thread_create(void (*func)())
{
struct thread *t;
for (t = all_thread; t < all_thread + MAX_THREAD; t++) {
if (t->state == FREE) break;
}
t->state = RUNNABLE;
// YOUR CODE HERE
}
// 3. in uthread_switch.S
/*
* save the old thread's registers,
* restore the new thread's registers.
*/
.globl thread_switch
thread_switch:
/* YOUR CODE HERE */
ret /* return to ra */感想
意外地很简单()在此不多做赘述,直接上代码。
+唯一要注意的一点就是记得在
+itrunc
中free掉+
代码
修改定义
-
// in fs.h
// On-disk inode structure
struct dinode {
// ...
uint addrs[NDIRECT+2]; // Data block addresses
};这几个函数到时候会被如此调用:
-+
int
main(int argc, char *argv[])
{
a_started = b_started = c_started = 0;
a_n = b_n = c_n = 0;
thread_init();
thread_create(thread_a);
thread_create(thread_b);
thread_create(thread_c);
thread_schedule();
exit(0);
}-
// in file.h
// in-memory copy of an inode
struct inode {
// ...
uint addrs[NDIRECT+2];
};所以,我们在第一个地方要做的,就是要填入swtch的签名。第二个地方要做的,就是要想办法让该线程一被启动就去执行参数的函数指针。第三个地方要做的,就是要完成上下文的切换。
-所以思路其实是很直观的。我们可以模仿进程管理中用来表示上下文的context,在
-thread_create
的时候把里面的ra设置为参数的函数指针入口,sp修改为thread结构体中的栈地址。swtch函数则完全把kernel/swtch.S
超过来就行。--在这个思路中,我们是怎么做到栈的切换的呢?
-每个线程在
-thread_create
的时候,都将自己的context中的sp修改为自己的栈地址。这样一来,在它们被调度的时候,switch会自然而然地从context中读取sp作为之后运行的sp,这样就实现了栈的切换。坑
我觉得其他方面都不难,最坑最细节的【也是我完全没有想到的……】就是这里:
-+
// 修改sp为栈顶
t->context.sp = (uint64)t->stack + STACK_SIZE;修改bmap()
-
// in fs.c
// 调试用
static int cnt = 0;
static uint
bmap(struct inode *ip, uint bn)
{
uint addr, *a;
struct buf *bp;
if(bn < NDIRECT){
if((addr = ip->addrs[bn]) == 0)
ip->addrs[bn] = addr = balloc(ip->dev);
return addr;
}
bn -= NDIRECT;
if(bn < NINDIRECT){
// Load indirect block, allocating if necessary.
if((addr = ip->addrs[NDIRECT]) == 0)
ip->addrs[NDIRECT] = addr = balloc(ip->dev);
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
if((addr = a[bn]) == 0){
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
// CODE HERE
bn -= NINDIRECT;
if(bn < NDOUBLEINDIRECT){
// 调试用
if(bn/10000 > cnt){
cnt++;
printf("double_indirect:%d\n",bn);
}
// 第一层
if((addr = ip->addrs[NDIRECT+1]) == 0)
ip->addrs[NDIRECT+1] = addr = balloc(ip->dev);
// 第二层
bp = bread(ip->dev,addr);
a = (uint*)bp->data;
if((addr = a[(bn >> 8)]) == 0){
a[(bn >> 8)] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
// 第三层
bp = bread(ip->dev,addr);
a = (uint*)bp->data;
if((addr = a[(bn & 0x00FF)]) == 0){
a[(bn & 0x00FF)] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
panic("bmap: out of range");
}需要注意,栈顶并不是
-t->stack
。通过测试程序:
-+
int main(){
int a[5]={1,2,3,4,5};
for(int i=0;i<5;i++){
printf("%p\n",&a[i]);
}
return 0;
}
0062feb8
0062febc
0062fec0
0062fec4
0062fec8修改itrunc
-
// Truncate inode (discard contents).
// Caller must hold ip->lock.
void
itrunc(struct inode *ip)
{
int i, j;
struct buf *bp;
uint *a;
for(i = 0; i < NDIRECT; i++){
if(ip->addrs[i]){
bfree(ip->dev, ip->addrs[i]);
ip->addrs[i] = 0;
}
}
if(ip->addrs[NDIRECT]){
bp = bread(ip->dev, ip->addrs[NDIRECT]);
a = (uint*)bp->data;
for(j = 0; j < NINDIRECT; j++){
if(a[j])
bfree(ip->dev, a[j]);
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT]);
ip->addrs[NDIRECT] = 0;
}
// CODE HERE
if(ip->addrs[NDIRECT+1]){
bp = bread(ip->dev, ip->addrs[NDIRECT+1]);
a = (uint*)bp->data;
// 双层循环
for(j = 0; j < NINDIRECT; j++){
if(a[j]){
struct buf* tmp_bp = bread(ip->dev,a[j]);
uint* tmp_a = (uint*)tmp_bp->data;
for(int k = 0;k < NINDIRECT; k++){
if(tmp_a[k])
bfree(ip->dev,tmp_a[k]);
}
brelse(tmp_bp);
bfree(ip->dev,a[j]);
}
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT+1]);
ip->addrs[NDIRECT+1] = 0;
}
ip->size = 0;
iupdate(ip);
}栈是向下增长的,因而,栈顶确实应该是数组的末尾……
-这里完全没有想到,还是吃了基础的亏啊。
+Symbolic links
+In this exercise you will add symbolic links to xv6.
+Symbolic links (or soft links) refer to a linked file by pathname; when a symbolic link is opened, the kernel follows the link to the referred file.
+Symbolic links resembles hard links, but hard links are restricted to pointing to file on the same disk, while symbolic links can cross disk devices.
+Although xv6 doesn’t support multiple devices, implementing this system call is a good exercise to understand how pathname lookup works.
+You will implement the
+symlink(char *target, char *path)
system call, which creates a new symbolic link at path that refers to file named by target. For further information, see the man page symlink. To test, add symlinktest to the Makefile and run it.--如果这里将
-t->stack
作为sp,那么运行时会出现非常诡异的现象(打印的是abc三个的thread->state
):-
仅有c【经测试,是仅有最后一个启动的线程】在执行,而ab的state都不是理想中的2,而是很奇怪的值。我确实有想过栈溢出问题,但是马上被我否定了。我完全没有想到是那样错的【悲】
+ +硬链接不会创建新的物理文件,但是会使得当前物理文件的引用数加1。当硬链接产生的文件存在时,删除源文件,不会清除实际的物理文件,即对于硬链接“生成的新文件”不会产生任何影响。
+软链接就更像一个指针,只是指向实际物理文件位置,当源文件移动或者删除时,软链接就会失效。
+【所以说,意思就是软链接不会让inode->ulinks++的意思?】
关于swtch
Update,验收时学长问为什么这里的uswitch.S为什么无需保存tn这样的寄存器。答案是因为tn是caller-save的,线程这相当于仅仅是执行一个函数,所以只需保存callee-save的寄存器。
-内核的swtch也只保存了这些callee-save的寄存器,也是同一个道理。
--
-
tn寄存器被保存在调用者的栈帧中。感觉也能理解为什么那题作业题说上文进程的现场是由栈保存了。
-代码
增加context结构体定义,修改thread结构体
+
struct context {
uint64 ra;
uint64 sp;
// callee-saved
uint64 s0;
uint64 s1;
uint64 s2;
uint64 s3;
uint64 s4;
uint64 s5;
uint64 s6;
uint64 s7;
uint64 s8;
uint64 s9;
uint64 s10;
uint64 s11;
};
struct thread {
char stack[STACK_SIZE]; /* the thread's stack */
int state; /* FREE, RUNNING, RUNNABLE */
struct context context;
};感想
这个实验比上个实验稍难一些,但也确实只是moderate的水平,其复杂程度主要来源于对文件系统的理解,还有如何判断环,以及对锁的获取和释放的应用。我做这个实验居然是没看提示的【非常骄傲<-】,让我有一种自己水平上升了的感觉hhh
+正确思路
本实验要求实现软链接。首先需要实现创建软链接:写一个系统调用
+symlink(char *target, char *path)
用于创建一个指向target的在path的软链接;然后需要实现打开软链接进行自动的跳转:在sys_open
中添加对文件类型为软链接的特殊处理。初见思路
我的初见思路是觉得可以完全参照
+sys_link
来写。但其实还是很不一样的。+
sys_link
的逻辑:+
+- 获取old的inode
+- 获取new所在目录的inode,称为dp
+- 在dp中添加一项entry指向old
++
sys_symlink
的逻辑:+
+- +
通过path创建一个新的inode,作为软链接的文件
+这里选择新建inode,而不是像link那样做,主要还是为了能遵从
+symlinktest
给的接口使用方法(朴实无华的理由)。而且这么做也很方便,符合“一切皆文件”的思想,也能简单化对其在open
中的处理。- +
在inode中填入target的地址
+我们可以把软链接视为文件,文件内容是其target的path。
+可以说是毫不相干,所以还是直接自起炉灶比较好。
+一些错误
其实没什么好说的,虽然debug过程挺久,但是靠常规的printf追踪就都可以看出来是哪里错了。下面我说说一个我印象比较深刻的吧。
++
symlinktest
中有一个检测点是,软链接不能成环,也即b->a->b是非法的。于是,我就选择了用快慢指针来检测环形链表这个思想,用来看是否出现环。在
+symlinktest
的另一个检测点中:+
我出现了如下错误:
++
此时的结构是1[27]->2[28]->3[29]->4,[]内为inode的inum。
+快慢指针的实现方式是当cnt为奇数的时候,慢指针才会移动。而上图中,cnt==0时,两个指针的值都发生了变化,这就非常诡异。
+这其实是因为slow指针所指向的那个inode被释放了,然后又被fast指针的下一个inode捡过来用了,从而导致值覆盖。
+为什么会被释放呢?
+-
// 快指针移动
readi(ip,0,(uint64)path,0,MAXPATH);
iunlock(ip);
if((ip = namei(path)) == 0){
end_op();
return -1;
}
// 在这里!!!
ilockput(ip);修改thread_create
+
void
thread_create(void (*func)())
{
struct thread *t;
for (t = all_thread; t < all_thread + MAX_THREAD; t++) {
if (t->state == FREE) break;
}
t->state = RUNNABLE;
// YOUR CODE HERE
// 将当前上下文保存入context
thread_switch((uint64)(&(t->context)),(uint64)(&(t->context)));
// 修改sp为栈顶
t->context.sp = (uint64)t->stack + STACK_SIZE;
// 修改ra为参数的函数指针入口
t->context.ra = (uint64)func;
}在这里,我错误地调用了
+ilockput
,从而使inode的ref–,使得它在下一次fast指针调用namei
,namei
调用iget
时,该inode被当做free inode使用,于是就这么寄了。所以我们需要把
+ilockput
的调用换成ilock
,这样一来就能防止inode被free。至于什么时候再iput?我想还是交给操作系统启动时的清理工作来做吧23333【开摆】代码
+
添加定义
fcntl.c
open参数
+-
// 意为只用获取软链接文件本身,而不用顺着软链接去找它的target文件修改thread_schedule
+
if (current_thread != next_thread) { /* switch threads? */
next_thread->state = RUNNING;
t = current_thread;
current_thread = next_thread;
/* YOUR CODE HERE
* Invoke thread_switch to switch from t to next_thread:
* thread_switch(??, ??);
*/
thread_switch((uint64)(&(t->context)),(uint64)(&(current_thread->context)));
} else
next_thread = 0;stat.h
文件类型
+-
修改thread_switch
全部照搬
-kernel/swtch.S
,没什么好说的Using threads
Barrier
--In this assignment you’ll implement a barrier: a point in an application at which all participating threads must wait until all other participating threads reach that point too.
-直接上代码,还是比较简单的
-+
static void
barrier()
{
// YOUR CODE HERE
//
// Block until all threads have called barrier() and
// then increment bstate.round.
//
pthread_mutex_lock(&(bstate.barrier_mutex));
bstate.nthread++;
while(bstate.nthread < nthread){
pthread_cond_wait(&(bstate.barrier_cond), &(bstate.barrier_mutex));
goto end;
}
// 此部分仅一个线程会进入
pthread_cond_broadcast(&(bstate.barrier_cond));
bstate.nthread = 0;
bstate.round++;
end:
pthread_mutex_unlock(&(bstate.barrier_mutex));
}添加sys_symlink系统调用
-
// in sysfile.c
uint64
sys_symlink(void)
{
char target[MAXPATH], path[MAXPATH];
struct inode *ip;
if(argstr(0, target, MAXPATH) < 0 || argstr(1, path, MAXPATH) < 0)
return -1;
begin_op();
// 创建软链接结点
ip = create(path,T_SYMLINK,0,0);
//printf("symlink:before writei,inum = %d\n",ip->inum);
// 此处可以防止住一些并发错误
if(ip ==0){
end_op();
return 0;
}
// 向软链接结点文件内写入其所指向的路径
writei(ip,0,(uint64)target,0,MAXPATH);
//printf("symlink:after writei\n");
// 软链接不需要让nlink++
// 记得要释放在create()中申请的锁
iunlockput(ip);
end_op();
return 0;
}--]]>Update
-关于pthread_cond的实现,也是使用了条件变量的思想,这个值得以后有时间了解一下。
-
总耗时:120h 约27天
-部分地方的翻译和表格来源参考:xv6指导书翻译
-部分文本来自:操作系统实验指导书 - 2023秋季 | 哈工大(深圳)
-实验官网:6.S081
-代码以github为准,此处记录的有些小瑕疵
-笔记的结构【以第一章Operating system interface为例】:
---懂了!VMware/KVM/Docker原来是这么回事儿这篇文章对虚拟化、虚拟机技术讲解很到位,写得通俗易懂,非常值得一看
-KVM 的「基于内核的虚拟机」是什么意思?这篇文章对QEMU-KVM架构进行了详细的介绍。还有这篇文章对应的知乎问题下面的高赞回答有机会也可以去看看。
-QEMU/KVM原理概述这篇文章前面的原理和上面那个差不多,后面有使用kvm做一个精简内核的实例,有兴趣/有精力/有需要可以看看。
- - -
以前只是知道,xv6是运行在qemu提供的虚拟环境之上的。qemu是什么,怎么虚拟的,虚拟机和宿主机是怎么交互的,这些一概不通。今天心血来潮想研究下qemu,虚拟机啥的到底是什么玩意,虽然看得有些猪脑过载,但还是写下一些个人的整理。
-在了解qemu之前,可以先了解一下虚拟化的思想。
---虚拟化的主要思想是,通过分层将底层的复杂、难用的资源虚拟抽象成简单、易用的资源,提供给上层使用。
-本质上,计算机的发展过程也是虚拟化不断发展的过程,底层的资源或者通过空间的分割,或者通过时间的分割,将下层的资源通过一种简单易用的方式转换成另一种资源,提供给上层使用。
-虚拟化可分为以下几方面:
+修改open
+ +
uint64
sys_open(void)
{
// ...
begin_op();
if(omode & O_CREATE){
ip = create(path, T_FILE, 0, 0);
if(ip == 0){
end_op();
return -1;
}
} else {
// 软链接不可能是以O_CREATE的形式创建的
if((ip = namei(path)) == 0){
end_op();
return -1;
}
ilock(ip);
if(ip->type == T_DIR && omode != O_RDONLY){
iunlockput(ip);
end_op();
return -1;
}
// 修改从这里开始
// 快慢指针
// ip为快指针,slow为慢指针
uint cnt = 0;
struct inode* slow = ip;
// 可能有多重链接,因而需要持续跳转
while(ip->type == T_SYMLINK){
//printf("slow = %d,fast = %d,cnt = %d\n",slow->inum,ip->inum,cnt);
// 其实这个只需要检测一次就够了。但为了编码方便,仍然把它保留在while循环中
if(omode & O_NOFOLLOW){
break;
}else{
// 检测到cycle
if(slow == ip && cnt!=0){
iunlockput(ip);
end_op();
return -1;
}
// 快指针移动
readi(ip,0,(uint64)path,0,MAXPATH);
// 此处不能用iunlockput(),具体原因见 感想-一些错误
iunlock(ip);
if((ip = namei(path)) == 0){
end_op();
return -1;
}
ilock(ip);
// 慢指针移动
// 注意,我慢指针移动的时候没有锁保护,因为用锁太麻烦了()其实还是用锁比较合适
if(cnt & 1){
//printf("%d\n",cnt);
readi(slow,0,(uint64)path,0,MAXPATH);
if((slow = namei(path) )== 0){
end_op();
return -1;
}
}
cnt++;
}
}
// 当跳出循环时,此时的ip必定是锁住的
}
if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
iunlockput(ip);
end_op();
return -1;
}
// ...
}Lab mmap
+-The
+mmap
andmunmap
system calls allow UNIX programs to exert detailed control over their address spaces.They can be used to:
-
+- CPU抽象:机器码、汇编语言到C语言、再到高级语言的不断虚拟的过程
-- 存储抽象:操作系统通过文件和目录抽象
-- 网络抽象:TCP/IP协议栈模型将网卡设备中传递的二进制数据,经过网络层、传输层的抽象后,为应用程序提供了便捷的网络包处理接口,而无需关心底层的IP路由、分片等细节
-- 进程抽象:操作系统通过进程抽象为不同的应用程序提供了安全隔离的执行环境,并且有着独立的CPU和内存等资源
+- share memory among processes
+- map files into process address spaces
+- as part of user-level page fault schemes such as the garbage-collection algorithms discussed in lecture.
In this lab you’ll add
+mmap
andmunmap
to xv6, focusing on memory-mapped files.mmap是系统调用,在用户态被使用。我们这次实验仅实现mmap功能的子集,也即memory-mapped files。
虚拟化的思想实际上就是我以前一直称为“抽象”的思想,以接口的形式逐层向上服务。
-虚拟机
--虚拟机的核心能力在于提供一个执行环境(隐藏底层细节),并在其中完成用户的指定任务。
---虚拟机有多种不同的形式,包括提供指令执行环境的进程、模拟器和高级语言虚拟机,或者是提供一个完整的系统环境的系统虚拟机。
-进程
进程实际上就是一种虚拟机。
--进程可以看作是一组资源的集合,有自己独立的进程地址空间以及独立的CPU和寄存器,执行程序员编写的指令,完成一定的任务。
-操作系统可以创建多个进程,每一个进程都可以看成一个独立的虚拟机,它们在执行指令、访问内存的时候并不会相互影响影响。
-模拟器
高级语言虚拟机
系统虚拟机
--通过系统虚拟化技术,能够在单个的宿主机硬件平台上运行多个虚拟机,每个虚拟机都有着完整的虚拟机硬件,如虚拟的CPU、内存、虚拟的外设等,并且虚拟机之间能够实现完整的隔离。
-在系统虚拟化中,管理全局物理资源的软件叫作虚拟机监控器(Virtual Machine Monitor,VMM),VMM之于虚拟机就如同操作系统之于进程,VMM利用时分复用或者空分复用的办法将硬件资源在各个虚拟机之间进行分配。
-qemu
可以看到,qemu就是一种虚拟机。它可以模拟虚拟机硬件,为操作系统提供虚拟硬件环境,从而能够让不同的操作系统能够在不同主机硬件上执行。
-qemu-kvm架构
诞生的原因
--其对于虚拟化技术的优化,以及发展的前因后果,具体可以看懂了!VMware/KVM/Docker原来是这么回事儿这篇文章。
-概括来讲,大致有以下几个要点:
-两种虚拟化方案
-
-
实现上述的虚拟化方案
一个典型的做法是——
-陷阱 & 模拟
技术什么意思?简单来说就是正常情况下直接把虚拟机中的代码指令放到物理的CPU上去执行,一旦执行到一些敏感指令,就触发异常,控制流程交给VMM,由VMM来进行对应的处理,以此来营造出一个虚拟的计算机环境。
-x86架构的问题
x86架构使得上述做法用不了了。因为它引入了四种权限
--
解决方法
-
全虚拟化
-VMware的二进制翻译技术、QEMU的模拟指令集
+declaration for
+mmap
:+ +
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);+
-参数
++
-- -
+
addr
is always zero.You can assume that
addr
will always be zero, meaning that the kernel should decide the virtual address at which to map the file.【addr
由kernel决定,因而用户态只需传入0即可】半虚拟化
+- -
+
length
is the number of bytes to mapMight not be the same as the file’s length.
硬件辅助虚拟化
-硬件辅助虚拟化细节较为复杂,简单来说,新一代CPU在原先的Ring0-Ring3四种工作状态之下,再引入了一个叫工作模式的概念,有
-VMX root operation
和VMX non-root operation
两种模式,每种模式都具有完整的Ring0-Ring3四种工作状态,前者是VMM运行的模式,后者是虚拟机中的OS运行的模式。qemu-kvm架构正是借助于此实现的。
+- -
+
prot
indicates whether the memory should be mapped readable, writeable, and/or executable.you can assume that
prot
isPROT_READ
orPROT_WRITE
or both.kvm
kvm就是借助硬件辅助虚拟化诞生的。可以把kvm看作是一堆系统调用。
-- --KVM本身是一个内核模块,它导出了一系列的接口到用户空间,用户态程序可以使用这些接口创建虚拟机。
-具体而言,KVM 可帮助您将 Linux 转变为虚拟机监控程序,使主机计算机能够运行多个隔离的虚拟环境,即虚拟客户机或虚拟机(VM)。【也即,虚拟机—进程,KVM—操作系统】
---在虚拟化底层技术上,KVM和VMware后续版本一样,都是基于硬件辅助虚拟化实现。不同的是VMware作为独立的第三方软件可以安装在Linux、Windows、MacOS等多种不同的操作系统之上,而KVM作为一项虚拟化技术已经集成到Linux内核之中,可以认为Linux内核本身就是一个HyperVisor,这也是KVM名字的含义,因此该技术只能在Linux服务器上使用。
-qemu-kvm
KVM本身基于硬件辅助虚拟化,仅仅实现CPU和内存的虚拟化,但一台计算机不仅仅有CPU和内存,还需要各种各样的I/O设备,不过KVM不负责这些。这个时候,QEMU就和KVM搭上了线,经过改造后的QEMU,负责外部设备的虚拟,KVM负责底层执行引擎和内存的虚拟,两者彼此互补,成为新一代云计算虚拟化方案的宠儿。
-qemu-kvm总体架构
KVM只负责最核心的CPU虚拟化和内存虚拟化部分;QEMU作为其用户态组件,负责完成大量外设的模拟。
--
VMX root和VMX non root
--VMX root是宿主机模式,此时CPU在运行包括QEMU在内的普通进程和宿主机的操作系统内核;
-VMX non-root是虚拟机模式,此时CPU在运行虚拟机中的用户程序和操作系统代码。
-也就是说,虚拟机的程序,包括用户程序和内核程序,都运行在non-root模式。宿主机的所有程序,包括用户程序【包括qemu】和内核程序【包括kvm】,都运行在root模式。
-qemu层(左上)
上面说到,qemu负责的是大量外设的模拟。它具体要做以下几件事:
--初始化虚拟机:
+- -
flags
has two values.-
创建模拟的芯片组
+- -
+
MAP_SHARED
meaning that modifications to the mapped memory should be written back to the file,
+如果标记为此,则当且仅当file本身权限为RW或者WRITABLE的时候,prot才可以标记为PROT_WRITE
创建CPU线程来表示虚拟机的CPU
-QEMU在初始化虚拟机的CPU线程时,首先设置好相应的虚拟CPU寄存器的值,然后调用KVM的接口将虚拟机运行起来,这样CPU线程就会被调度在物理CPU上执行虚拟机的代码。
+- -
+
MAP_PRIVATE
meaning that they should not.
+如果标记为此,则无论file本身权限如何,prot都可以标记为PROT_WRITE
在QEMU的虚拟地址空间中分配空间作为虚拟机的物理地址
+根据用户在命令行指定的设备为虚拟机创建对应的虚拟设备【如各种IO设备】
+You can assume
offset
is zero (it’s the starting point in the file at which to map)虚拟机运行时:
--
+- -
监听多种事件
-包括虚拟机对设备的I/O访问、用户对虚拟机管理界面、虚拟设备对应的宿主机上的一些I/O事件(比如虚拟机网络数据的接收)等
调用函数处理
+return
+
mmap
returns that kernel-decided address, or 0xffffffffffffffff if it fails.如果两个进程同时对某个文件进行memory map,那么这两个进程可以不共享物理页面。
可以看到,qemu确实利用了宿主机的各种资源,提供了一个很完美的硬件环境。其资源对应关系为:
-虚拟机的CPU——宿主机的一个线程
-虚拟机的物理地址——qemu在宿主机的虚拟地址
-虚拟机对硬件设备的访问 —→ 对qemu的访问
-它大概做了两件事:
++++
munmap(addr, length)
should remove mmap mappings in the indicated address range.If the process has modified the memory and has it mapped
+MAP_SHARED
, the modifications should first be written to the file. 【如果两个进程的修改发生冲突了怎么办?】An
+munmap
call might cover only a portion of an mmap-ed region, but you can assume that it will either unmap at the start, or at the end, or the whole region (but not punch a hole in the middle of a region).
这个实验做得我……怎么说,感觉非常地难受吧。虽然我认为我这次做得挺不错的,因为我没有怎么看hints,我的代码差不多全都是我自己想出来的,没有依赖保姆级教学,我认为是一个很好的进步。不过,正因为我没有看hints,导致我的想法比起答案来思路非常地奇诡,导致我第一次错误想法写了一天,看了hints后决心痛改前非,结果第二次错误想法又写了一天emmm
+下面的第一个代码版本虽然可以过掉mmaptest,但确实还是有一个很致命的bug,并且lazy也没有lazy到位,最后的版本离正确思路还有偏差,也就是下面放的第一个代码版本是错误的,但我认为它也不是完全没有亮点。第二个版本才是经过改正的正确版本,但写得着实有点潦草。
+笔记整理得也有点匆忙,毕竟我真的话比较多而且心里很烦。总之,先记录我的全部思路过程,至于价值如何,先不管了2333
+所以说,我们要做的,就是实现一个系统调用mmap,在mmap中,应该首先申请几页用来放file的内容,并且在页表中填入该项,然后再返回该项的虚拟地址。然后在munmap中,再将该file页内容写入file。
+也就是说,直接在mmap把文件的全部内容写入内存,然后各进程读写自己的那块内容块,最后在munmap的时候把修改内容写入文件然后释放该内存块就行了
+题目要求the kernel should decide the **virtual address** at which to map the file.
也就是说,在我们的mmap
中,需要决定我们要讲文件内容放在哪里。那要放在哪呢……
我第一反应很奇葩:扫描页表,找到空闲页。但我自己也知道这样不可行,文件内容不止一页,这种零零散散存储需要的数据结构实现起来太麻烦了。
+那怎么办?可以在heap内分配。那么到底怎么样才能在heap里分配?你该怎么知道heap哪里开始是空闲的,哪里是用过的,不还是得扫描页表吗?【思维大僵化】
+其实……道理很简单。我们之间把proc->sz
作为mapped-file的起始地址就好了。相信看到这里,你也明白这是什么原理了。能想到这个,我感觉确实很不容易。
初见思路虽然简单,但是很粗暴,如果文件很大,宝贵的内存空间就会被我们浪费。所以我们借用lazy allocation的思想,先建立memory-file的映射,再在缺页中断中通过文件读写申请内存空间,把文件内容读入内存。
+问题就在于如何“先建立memory-file的映射”。在lazy allocation中,我们是先填好所有的对应页表项,仅是不申请对应的物理内存,也即占着XX不XX。在这次实验中,我们也是这么做,只不过新增了一个难点,那就是如何管理这些页。因为lazy allocation页与页之间没有比较紧密的关系,但是在mmap中页却可以被所属文件这个关键字划分。因而,我们需要一个数据结构,来给页分门别类地组织在一起,并且记录它们的meta data比如说所属文件之类的,这也就是hints里的VMA结构,也即我的filemap结构。
+我们可以将这样的数据结构池化,并且存储在proc域中,以避免对象的重复创建。
+++我的lazy法与别人不大一样……我没有想得像他们那么完美。我的做法是,在需要读某个地址的文件内容时,直接确保这个地址前面的所有文件内容都读了进来。也即在filemap中维护一个okva,表明va
+okva这段内存已经读入,之后就仅需再读入okvaneed_va这段地址就行。这样虽然lazy了,但没完全lazy。我认为这不能体现lazy的思想……因为一读读一坨,还是很占空间啊。
+
因而,我们需要做的就是:
给qemu提供运行时的参数
-通过“/dev/kvm”设备,比如CPU个数、内存布局、运行等。
-截获VM Exit事件【下面会讲,用来完成虚拟机和硬件环境的交互】并进行处理。
-在mmap中将信息填入该数据结构
+CPU——QEMU进程中的一个线程
-通过QEMU和KVM的相互协作,虚拟机的线程会被宿主机操作系统正常调度,直接执行虚拟机中的代码
-物理地址——QEMU进程中的虚拟地址
-设备——QEMU实现
-在运行过程中,虚拟机操作系统通过设备的I/O端口(Port IO、PIO)或者MMIO(Memory Mapped I/O)进行交互,KVM会截获这个请求【也即VM Exit,下面会讲】,大多数时候KVM会将请求分发到用户空间的QEMU进程中,由QEMU处理这些I/O请求
+在我的代码中,还针对proc->sz不满足page-align做出了对策:先把文件的PGROUNDUP(sz)-sz
这部分的信息读入,并且更新okva,这样一来,之后在usertrap中,就可以从okva开始一页页地分配地址,做到自然地page-align了。
+为什么要对不满足page-align的情况进行处理?
+这是因为,growproc的时候一次性扩充一页,但proc->sz却可以不满足page-align,也就是说,proc->sz所处的这一页已经被分配了。
+在我们的lazy思路中,我们如果不预先读入文件页,是只能等待用户陷入缺页中断的情况下才能读入文件内容。
+但是,proc->sz这一页已经被分配了。因而,在用户态读取这一页地址的时候,并不会发生缺页中断。因而,就会发生文件内容未读入,用户读到脏数据的情况。
+其实还有一种更简单的办法可以强制page-align,那就是,直接让起始地址为
+PGROUNDUP(proc->sz)
……至于为什么我不用这个,而要写这么多麻烦的东西呢?答案是我没想到。()
在usertrap增加对缺页中断的处理
+readi
(正确)fileread
(错误)读取文件内容并存入物理内存虚拟机肯定是会与它的硬件环境进行交互的,它的硬件环境也就是QEMU—KVM。
-虚拟机的用户程序和内核程序都是直接由宿主机的操作系统正常调度,我们可以将其看作虚拟态。QEMU—KVM可以看作是宿主机的进程,我们可以将其看作宿主态。因而,当虚拟机一些事情希望由QEMU—KVM来做,我们就需要从虚拟态转移到宿主态。
-听起来有没有感觉很耳熟?是的,“从用户态陷入内核态”,跟这个的原理是一样的。
-因而,虚拟机与硬件环境交互,实际上是虚拟态和宿主态状态的转换,如下图:
-VM Exit
-当虚拟机中的代码是敏感指令或者说满足了一定的退出条件时,CPU会从虚拟态退出到KVM,这叫作VM Exit。
-这就像在用户态执行指令陷入内核一样。
-VM Exit首先陷入到KVM中进行处理,如果KVM无法处理,比如说虚拟机写了设备的寄存器地址,那么KVM会将这个写操作分派到QEMU中进行处理。
-VM Entry
-当KVM或者QEMU处好了退出事件之后,又可以将CPU置于虚拟态以运行虚拟机代码,这叫作VM Entry。
---QEMU在初始化的时候会通过
-mmap
分配虚拟内存空间作为虚拟机的物理内存,【感觉思路打开,物理内存与文件对应了起来】QEMU在不断更新内存布局的过程中会持续调用KVM接口通知内核KVM模块虚拟机的内存分布。
虚拟机在运行过程中,首先需要将虚拟机的虚拟地址(Guest Virtual Address,GVA)转换成虚拟机的物理地址(Guest Physical Address,GPA),然后将虚拟机的物理地址转换成宿主机的虚拟地址(Host Virtual Address,HVA),最终转换成宿主机的物理地址(Host Physical Address,HPA)。
-整个寻址过程由硬件实现,具体实现方式为扩展页表(Extended Page Table,EPT)。
-在支持EPT的环境中,虚拟机在第一次访问内存的时候就会陷入到KVM,KVM会逐渐建立起所谓的EPT页面【lazy思想贯穿始终,还是该叫自适应?】。这样虚拟机的虚拟CPU在后面访问虚拟机虚拟内存地址的时候,首先会被转换为虚拟机物理地址,接着会查找EPT页表,然后得到宿主机物理地址。【有种TLB的感觉】
-设备模拟的本质是要为虚拟机提供一个与物理设备接口完全一致的虚拟接口。
-虚拟机中的操作系统与设备进行的数据交互或者由QEMU和(或)KVM完成,或者由宿主机上对应的后端设备完成。
-QEMU在初始化过程中会创建好模拟芯片组和必要的模拟设备,包括南北桥芯片、PCI根总线、ISA根总线等总线系统,以及各种PCI设备、ISA设备等。
-外设虚拟化主要有如下几种方式:
+在munmap中进行释放
纯软件模拟(完全虚拟化)
-QEMU最早的方案,虚拟机内核不用做任何修改,每一次对设备的寄存器读写都会陷入到KVM,进而到QEMU,QEMU再对这些请求进行处理并模拟硬件行为。
-软件模拟会导致非常多的QEMU/KVM接入,效率低下。
+virtio设备(半虚拟化)
-virtio设备是一类特殊的设备,并没有对应的物理设备,所以需要虚拟机内部操作系统安装特殊的virtio驱动。
-相比软件模拟,virtio方案提高了虚拟设备的性能。
+修改fork和exit
+exit
+手动释放map-file域
++为什么不能把这些合并到
+wait
中调用的freepagetable
进行释放呢?因为
+freepagetable
只会释放对应的物理页,没有达到munmap
减少文件引用等功能。
设备直通
-将物理硬件设备直接挂到虚拟机上,虚拟机直接与物理设备交互,尽可能在I/O路径上减少QEMU/KVM的参与。
-设备直通经常搭配硬件虚拟化支持技术SRIOV(Single Root I/O Virtualization,单根输入/输出虚拟化)使用,SRIOV能够将单个的物理硬件高效地虚拟出多个虚拟硬件。
+fork
+手动复制filemap池
操作系统通过写设备的I/O端口或者MMIO地址来与设备交互,设备通过发送中断来通知操作系统事件。
-QEMU/KVM一方面需要完成这项中断设备的模拟,另一方面需要模拟中断的请求处理。
+上面说到:
--QEMU支持单CPU的Intel 8259中断控制器以及SMP的I/O APIC(I/O Advanced Programmable Interrupt Controller)和LAPIC(Local Advanced Programmable Interrupt Controller)中断控制器。在这种方式下,虚拟外设通过QEMU向虚拟机注入中断,需要先陷入到KVM,然后由KVM向虚拟机注入中断,这是一个非常费时的操作。
-为了提高虚拟机的效率,KVM自己也实现了中断控制器Intel 8259、I/O APIC以及LAPIC。用户可以有选择地让QEMU或者KVM模拟全部中断控制器,也可以让QEMU模拟Intel 8259中断控制器和I/O APIC,让KVM模拟LAPIC。
+问题就在于如何“先建立memory-file的映射”。在lazy allocation中,我们是先填好所有的对应页表项,仅是不申请对应的物理内存,也即占着XX不XX。在这次实验中,我们也是这么做,只不过新增了一个难点,那就是如何管理这些页。因为lazy allocation页与页之间没有比较紧密的关系,但是在mmap中页却可以被所属文件这个关键字划分。因而,我们需要一个数据结构,来给页分门别类地组织在一起,并且记录它们的meta data比如说所属文件之类的,这也就是hints里的VMA结构,也即我的filemap结构。
介绍完上述的qemu虚拟化,接下来就可以对xv6的全启动进行一个梳理了。
-首先,在宿主机执行make qemu
。
在Makefile
中可以看到:
qemu: $K/kernel fs.img |
官方给出的答案是在proc域里的pool。我……额……是把这些信息,存入在页中(真是自找麻烦呀)
+具体来说,就是,我在mmap
的时候给每个文件申请一页,然后在页的开头填上和filemap结构相差无几的那些参数,再加上一个next指针,表示下一个文件页的地址。页的剩下部分就用来存储数据。总的就是一个链表结构。
这个思路其实很不错,比起上面的直接在proc内存的尾巴扩容,这个空间利用率应该更大,并且不仅能节省物理内存,还能节省虚拟地址空间,实现了lazy上加lazy。
+但问题是……我为什么非要傻瓜式操纵内存,在页的开头填入参数数据,而不是把这种页抽象为一个个node,最终形成一个十字链表的形式(差不多的意思,鱼骨状),组织进proc域,这样不挺好的吗……唔,有时候我头脑昏迷程度让我自己都感到十分震惊。归根结底,还是想得太少就动手了,失策。
+总之放上代码。我没有实现next指针,仅假设文件内容不超过一页。也就是这一页开头在mmap中填meta data,其余部分在usertrap中填入文件内容。【这个分开的点也让我迷惑至极……】
+
|
在log中可以看到:
-... |
} else if(r_scause() == 13 || r_scause() == 15){ |
具体的Makefile
相关内容我不大了解,但结合输出,我想大概是先通过riscv64-linux-gnu-gcc
编译链接完所有文件,然后再执行mkfs
产生fs.img
镜像(mkfs
后面那些东西应该是文件参数,对应于源码中的读取可执行程序进磁盘的部分),最后再运行qemu-system-riscv64
开始对虚拟机进行boot。
boot直至启动后的所有代码,都是通过QEMU-KVM架构处理,直接运行在宿主机的CPU上的。其余的各种管理,可以详见小标题虚拟机在QEMU-KVM架构的执行方法
。
上面的知识表明,操作系统的启动在于文件系统初始化之后,这是因为操作系统本身的启动代码,放在磁盘映像fs.img
中,而fs.img
正是由文件系统初始化时弄出来的。也就是说,文件系统是操作系统的爸爸。【我以前一直以为是反过来的】
正如开头所说的那样,我并没有完美做好这次实验,下面代码有一个致命的bug。
+先说说致命bug是什么。
+我的filemap结构体其实隐藏了两个具有“offset”这一含义的状态。一个是filemap里面的成员变量offset,另一个是filemap里面的成员变量file的成员变量off:
+// in proc.h |
在我的代码里,它们被赋予了不同的含义。
+filemap->file->off
被用于trap.c
中,表示的是当前未读入文件内容的起始位置(实际上也就是okva-va
的值),用于自然地使用fileread
进行文件读入。
--图中的boot块就是操作系统的引导扇区。
+比如说,这次读入PGSIZE,那么off就会在
fileread
中自增PGSIZE。下次调用fileread
就可以直接从下一个位置读入了,这样使代码更加简洁
而mkfs
的作用,正是把宿主机提供的虚拟地址空间作为虚拟磁盘,把虚拟地址空间划分为如上图所示的地址结构。它是运行在宿主机当中的。有了mkfs
,才能有我们的虚拟机。
yysy这个就写得很好了。
--linux的堆管理
-那么malloc到底是怎么实现的呢?不是每次要申请内存就调一下系统调用,而是程序向操作系统申请⼀块适当⼤⼩的堆空间,然后由程序⾃⼰管理这块空间,⽽具体来讲,管理着堆空间分配的往往是程序的运⾏库。
-也就是说,malloc本质上是以运行库而非系统调用形式出现的。它里面用到的是sbrk和mmap这两个系统调用来进货。
-glibc的malloc函数是这样处理⽤户的空间请求的:对于⼩于128KB的请求来说,它会在现有的堆空间⾥⾯,按照堆分配算法为它分配⼀块空间并返回;对于⼤于128KB的请求来说,它会使⽤mmap()函数为它分配⼀块匿名空间,然后在这个匿名空间中为⽤户分配空间。
++
filemap->offset
被用于munmap
中。filewrite
同fileread
一样,都是从file->off
处开始取数据。munmap
所需要取数据的起始位置和trap.c
中需要取数据的起始位置肯定不一样,+-想想它们的功能。
trap.c
的off需要始终指向有效内存段的末尾,但munmap
由于要对特定内存段进行写入文件操作,因而off要求可以随机指向。在内核态中,我们使用
-kalloc
和kfree
来申请和释放内存页。在用户态中,我们使用malloc
和free
来对动态内存进行管理。【也就是说这个实现的是堆管理】内核中的最小单位只能是页,但user mem-allocator对外提供的申请内存服务的最小单位不是页,而是
-sizeof(Header)
。因而,这就需要我们的user mem-allocator进行数据结构的管理,来统一这二者的实现。数据结构
环形链表
user mem-allocator的数据结构是环形链表,起始结点为一个空数据载体。
--
-
地址从低到高
链表的头结点的存储地址/所代表的内存地址的地址数值最小,并且其余结点按遍历顺序地址递增。
-具体实现
user mem-allocator由三个主要函数组成,分别是
-morecore
、malloc
和free
。一个一个地来说未免有点不符合正常人的思路,所以我接下来会以用户初次调用malloc
为例,来整理user mem-allocator的具体实现。malloc
当用户初次调用
-malloc
,此时freep仍为空指针,因而会进入如下分支:- -
if((prevp = freep) == 0){
// 空闲mem为空的情况
base.s.ptr = freep = prevp = &base;
base.s.size = 0;
}也即初始化为这种情况:
--
随后,由于
-prevp->ptr == freep
,故而会在循环中进入该分支:+
for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr){
// ...
if(p == freep) // 一般情况下,此处表明已经完整遍历了一遍环形链表,因为prev的初值是freep,而我们是从prev->next开始遍历的
if((p = morecore(nunits)) == 0)
return 0;
}因而,我们可以将当前va对应的文件位置记录在offset中。届时,我们只需要从
+p->filemaps[i].offset+va-p->filemaps[i].va
取数据就行。上述两个变量相辅相成,看上去似乎能够完美无缺地实现我们的功能。但是,实际上,不行。为什么呢?因为它们的file指针,
+filemap->file
,如果被两个mmap区域同时使用的话,就会出问题。可以来看看
+mmaptest.c
中的这一段代码:-
makefile(f);
if ((fd = open(f, O_RDONLY)) == -1)
err("open");
unlink(f);
char *p1 = mmap(0, PGSIZE*2, PROT_READ, MAP_SHARED, fd, 0);
char *p2 = mmap(0, PGSIZE*2, PROT_READ, MAP_SHARED, fd, 0);
// read just 2nd page.
if(*(p1+PGSIZE) != 'A')
err("fork mismatch (1)");
if((pid = fork()) < 0)
err("fork");
if (pid == 0) {
// v1是用来触发缺页中断的函数
_v1(p1);
munmap(p1, PGSIZE); // just the first page
exit(0); // tell the parent that the mapping looks OK.
}
int status = -1;
wait(&status);
if(status != 0){
printf("fork_test failed\n");
exit(1);
}
// check that the parent's mappings are still there.
printf("before v1,p1 = %d\n",(uint64)p1);
_v1(p1);
printf("after v1,p1 = %d\n",(uint64)p1);
_v1(p2);
printf("fork_test OK\n");
/*输出:
fork_test starting
trap:map a page at 53248,okva = 53248
trap:mem[0]=65,off = 4096,size = 6144
trap:map a page at 57344,okva = 53248
trap:mem[0]=65,off = 6144,size = 6144
before v1,p1 = 53248
after v1,p1 = 53248
trap:map a page at 61440,okva = 61440
trap:mem[0]=0,off = 6144,size = 6144
mismatch at 0, wanted 'A', got 0x0
mmaptest: fork_test failed: v1 mismatch (1), pid=3
*/调用
-morecore
。morecore
进入
-morecore
后,首先会对堆内存进行扩容:+
if(nu < 4096)
nu = 4096;
p = sbrk(nu * sizeof(Header));
if(p == (char*)-1)
return 0;-
// in trap.c
printf("trap:map a page at %d,okva = %d\n",start_va,p->filemaps[i].okva);
fileread(p->filemaps[i].file,start_va,PGSIZE);
printf("trap:mem[0]=%d,off = %d,size = %d\n",
mem[0],p->filemaps[i].file->off,p->filemaps[i].file->ip->size);其中,nu表示要申请的内存单元数,一个内存单元为
-sizeof(Header)
,因而nu在malloc
中计算如下:+
nunits = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1;这段代码因为共用fd,导致file指针被两个mmap区域同时使用。
+++共用fd,为什么file指针也一起共用了?
+可以追踪一下它们的生命周期:
+-
// in sys_open()
// 获取file结构体和文件描述符。
if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
// in sysfile.c
// Allocate a file descriptor for the given file.
// Takes over file reference from caller on success.
static int
fdalloc(struct file *f)
{
int fd;
struct proc *p = myproc();
for(fd = 0; fd < NOFILE; fd++){
if(p->ofile[fd] == 0){
p->ofile[fd] = f;
return fd;
}
}
return -1;
}为了满足内核以一页为最小内存单位的需求,以及避免过多陷入内核态,它每次会申请至少4096*内存单元的堆空间。
-对堆内存进行扩容完之后,
-morecore
会手动调用一次free
,将新申请到的内存加入数据结构中。【此处类似于在knit
中调用kfree
的原理】free
+
void free(void *ap){
Header *bp, *p;
bp = (Header*)ap - 1;
for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)
if(p >= p->s.ptr && (bp > p || bp < p->s.ptr))
break;可以看到,它实际上是有一个文件描述符表,key为fd,value为file指针。因而,同一张表,fd相同,file指针相同。
+注:父子进程,同样的fd,file指针也是相同的
+fork出来的父子进程同一个句柄对同一个文件的偏移量是相同的,这个原理应该是因为,父子进程共享的是文件句柄这个结构体对象本身,也就是拷贝的时候是浅拷贝而不是深拷贝。
++
// in fork()
// increment reference counts on open file descriptors.
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);最后的
+check that the parent's mappings are still there.
环节中,_v1(p1)
执行时并没有陷入trap,这是正常的。不正常的是_v1(p2)
的执行结果。它陷入了trap,但是却因file->off == file size
,导致被判定为已全部读入文件,事实上却是并没有读入文件。为什么会这样呢?
+这是因为p1和p2共用同一个fd,也就共用了同一个file指针。共用了一个file指针,那么p1和p2面对的
+file->off
相同。上面说到,file->off
用于控制文件映射。那么,当p1完成了对文件的映射,p1的off指针如果不加重置,就会永远停留在file size处。这样一来,当p2想要使用同样的file指针进行文件映射时,就会出问题。这个问题的一个解决方法是每次
+mmap
都深拷贝一个船新file结构体。但是这样的话,file
域里的ref
变量就失去了它的意义,并且file对象池应该也很快就会爆满,非常不符合设计方案。这个问题的完美解,是不要赋予
+file->off
这个意义,而是使用readi
替代fileread
。-
fileread(struct file *f, uint64 addr, int n)
readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n)由于此时
+freep == freep->str == base
,并且我们在morecore
中新申请的内存空间ap
满足ap > base
,故而会跳出循环。这样做的好处是,我们可以实时计算offset(前面提到,其恰恰等于okva-va),而不用把这个东西用file的off来表示。
--为什么
-ap > base
呢?别忘了我们扩容的原理。我们是以
+proc->size
为起始地址扩容的。ap处在扩容内存中,因而ap>旧size;base处在扩容前内存内,因而base<=旧size。故而有ap>base。也确实,我之所以弯弯绕绕那么曲折,是因为只想到了
fileread
这个函数,压根没注意到还有一个readi
……+
if(bp + bp->s.size == p->s.ptr){
bp->s.size += p->s.ptr->s.size;
bp->s.ptr = p->s.ptr->s.ptr;
} else
bp->s.ptr = p->s.ptr;
if(p + p->s.size == bp){
p->s.size += bp->s.size;
p->s.ptr = bp->s.ptr;
} else
p->s.ptr = bp;
freep = p;我在下面的代码仅做了一个能够通过测试,但是上面的bug依然存在的功利性折中代码。我是这么实现的:
+-
// 在`mmap`的时候初始化`file->off`
p->filemaps[i].file->off = offset;
// 在`munmap`的时候清零`file->off`
p->filemaps[i].file->off = 0;跳出循环后,我们会进入第一个if的第二个分支,以及第二个if的第二个分支。经过这些指针操作后,此时我们的数据结构如下图所示:
--
也即形成了一个两节点的环形链表。
-malloc
经历完上述调用后,我们回到
-malloc
的循环中:-
for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr){
// ...
if(p == freep)
if((p = morecore(nunits)) == 0)
return 0;
}由
-morecore
的返回值可知,此时我们的p应该指向freep。本轮循环结束后执行p = p->s.ptr
,此时我们的p指向了我们刚在morecore
中扩容出来的那一大段内存。-
在下一轮循环中,由于我们刚刚通过
-morecore
申请了至少nunits
的空间,因而我们将进入该分支:-
if(p->s.size >= nunits){
if(p->s.size == nunits)
// 如果与所需的内存刚好相等,那就直接返回该小单元就行
prevp->s.ptr = p->s.ptr;
else {
// 不等的话就只划分出一小部分
// 一次划出几个header单元
p->s.size -= nunits;
p += p->s.size;
p->s.size = nunits;
}
freep = prevp;
return (void*)(p + 1);
}当
-nunits >= 4096
,也即p->s.size == nunits
,p所指向的地址恰好就是我们接下来会用的地址。因而,我们就将这部分内存空间从我们的freelist中剔除,在之后返回p的地址即可。当
-nunits < 4096
,也即p->s.size != nunits
,说明p所指向的这块内存空间比我们需要的大,那么我们就仅将该段内存空间切割出需要的那一小部分,再把p指向那一小部分开头的地方,返回p地址即可,如图所示。-
这样一来,我们就成功给用户它所需要的内存空间了。
-free
进行malloc之后,用户还需要调用free来手动释放内存,防止内存泄漏。
--
+
for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr)
if(p >= p->s.ptr && (bp > p || bp < p->s.ptr))
break;因而,结论是,一步错步步错,一个错误需要更多的错误来弥补,最后还是错的(悲)
+如何把下面的错误思路改成正确思路
可以做以下几点:
++
+- +
正确地lazy
+每次trap仅分配一页。
+- +
改用readi函数,修改
+file->off
的语义这样一来,大概就可以完美地正确了。
+其他的一些小细节
file指针的生命周期
在数据结构中存储file指针至关重要。但仔细想一想,file指针的生命周期似乎长到过分:从sys_mmap被调用,一直到usertrap处理缺页中断,最后到munmap释放,我们要求file指针的值需要保持稳定不变。
+这么长的生命周期,它真的可以做到吗?毕竟file指针归根到底只是一个局部变量,在syscall mmap结束之后,它还有效吗?答案是有效的,这个有效性由
+mmap
实现中对ref的增加来实现保障。在用户态中关闭一个文件,需要使用syscall
+close(int fd)
。不妨来看看close
的代码。-
// in kernel/sysfile.c
uint64
sys_close(void)
{
int fd;
struct file *f;
if(argfd(0, &fd, &f) < 0)
return -1;
// 一个进程打开的文件都会放入一个以fd为index的文件表里,
// 在xv6中,这个文件表便是`myproc()->ofile`。
// 可以看到,关闭一个文件首先需要把它移出文件表
myproc()->ofile[fd] = 0;
// 对file指针关闭的主要操作
fileclose(f);
return 0;
}
// in kernel/file.c
// Close file f. (Decrement ref count, close when reaches 0.)
void
fileclose(struct file *f)
{
struct file ff;
acquire(&ftable.lock);
// 若ref数<0,就会直接return
if(--f->ref > 0){
release(&ftable.lock);
return;
}
// 释放file
// close不会显式地释放file指针,只会释放file指针所指向的文件,让file指针失效。
ff = *f;
f->ref = 0;
f->type = FD_NONE;
release(&ftable.lock);
if(ff.type == FD_PIPE){
pipeclose(ff.pipe, ff.writable);
} else if(ff.type == FD_INODE || ff.type == FD_DEVICE){
begin_op();
iput(ff.ip);
end_op();
}
}由于
+ap > base
且ap > 旧p->size = base->ptr
且base < base->ptr
,故而首先会进行一轮循环。再然后,由于p = 旧p->size
,并且p > p->ptr = base
,并且ap > 旧size
,故而跳出循环。可以看到,当ref数>1时,file指针就不会失效。
+这就是为什么我们还需要在mmap中让file的ref数++。
+缺页中断蕴含的设计思想
如果只存入file指针,用户态要如何对对应的文件进行读写呢?
+我们可以自然想到也许需要设计一个函数,让用户在想要对这块内存读写的时候调用这个函数即可。但是,这样的方法使得用户对内存不能自然地读写,还需要使用我们新设计的这个函数,这显然十分地不美观。所以,我们需要找到一个方法,让上层的用户可以统一地读取任何的内存块,包括memory-mapped file内存块,而隐藏memory-mapped file与其他内存块读写方式不同的这些复杂细节。经历过前面几次实验的你看到这里一定能想到,有一个更加优美更加符合设计规范的方法,那就是:缺页中断!
--此处循环中,循环语句内部的这个循环实际上是对遍历到环形链表尾部,即将从头开始遍历,这个边界情况的处理。比较符合逻辑的还是循环语句内的那个条件。
+没做这个实验之前就知道mmap需要借助缺页中断来实现了,但实际自己的第一印象是觉得并不需要缺页中断,直到分析到这里才恍然大悟。
+“让上层的用户可以统一地读取任何的内存块,而隐藏不同类型的内存块读写方式不同的这些复杂细节”
+仔细想想,前面几个关于缺页中断的实验,比如说cow fork,lazy allocation,事实上都是基于这个思想。它们并不是不能与缺页中断分离,只是有了缺页中断,它们的实现更加简洁,更加优美。
+再次感慨os的博大精深。小小一个缺页中断,原理那么简单,居然集中了这么多设计思想,不禁叹服。
+
if(bp + bp->s.size == p->s.ptr){
bp->s.size += p->s.ptr->s.size;
bp->s.ptr = p->s.ptr->s.ptr;
} else
bp->s.ptr = p->s.ptr;
if(p + p->s.size == bp){
p->s.size += bp->s.size;
p->s.ptr = bp->s.ptr;
} else
p->s.ptr = bp;
freep = p;正确答案的munmap中如果遇到未映射的页怎么办
在正确答案的munmap中:
+-
//释放已经申请的页表项、内存,并且看看是不是需要写回
while(start_va < bounder){
if(p->filemaps[i].flags == MAP_SHARED){
//写回
filewrite(p->filemaps[i].file,start_va,PGSIZE);
}
uvmunmap(p->pagetable,start_va,1,1);
start_va += PGSIZE;
}此时会进入第二个if的第一个分支。具体情况看图就行,不多bb。
-总结
主要就是这个数据结构用得很巧妙但也很复杂。它吸取了内核态中分配内存使用一个freelist的特点,同时又巧妙地利用了内存地址有序的特点,从而实现碎片内存管理。我的建议是多画图。
-还有其实有一点我不是很理解。我觉得
-freep
这个变量的用意非常不明,它似乎并不是指代整个freelist的头,因为它在很多个地方都诡异地赋值了一次。我想,它也许始终指向上一次被alloc/被free的内存的前一个吧。。。我猜测这样设计是为了蕴含一些LRU的思想。不大明白。m-s-u权限切换
由os知识可知,机器态、内核态、用户态分别有三种不同的操作权限。xv6是如何对权限切换进行管理的呢?
-这部分知识我在正文的一个小地方记录了下来,详见 chapter2 - Code: starting xv6 and the fifirst process - xv6 - 感想 的第二点。
-Lock实验的评测机制
在xv6该次实验中,为了实现评测可视化,引入了statistics机制对结果进行评估。下面,我将通过源码简单介绍其实现机制。
-来讲讲这玩意是怎么实现用户态读取锁争用次数的。我们从
-statistics
函数可看出,它的本质是通过读取“文件”,来从内核中读取争用次数的相关数据:+
int statistics(void *buf, int sz) {
fd = open("statistics", O_RDONLY);
...
if ((n = read(fd, buf+i, sz-i)) < 0) {
}如果map类型为
+MAP_SHARED
,并且该页尚未映射,会怎么样呢?追踪filewrite的路径
+-
// in file.c
begin_op();
ilock(f->ip);
if ((r = writei(f->ip, 1, addr + i, f->off, n1)) > 0)
f->off += r;
iunlock(f->ip);
end_op();
// in fs.c
if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
brelse(bp);
break;
}
log_write(bp);
// in vm.c copyin()
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(srcva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
// ...那么安装以前所学的内容,我们很容易联系到这玩意应该并不是个文件,而是类似于proc文件系统那样的虚拟文件。它应该会在open、read中根据其特有的文件类型进行转发。在
-init.c
中,我们可以看到:+
main(void)
{
if(open("console", O_RDWR) < 0){
mknod("console", CONSOLE, 0);
mknod("statistics", STATS, 0);
open("console", O_RDWR);
}+
copyin
最终会在if(pa0 == 0) return -1;
这里终结,但writei
并不会在接收到-1的时候爆出panic或者是引发缺页中断,而只会把它当做文件结尾,默默地返回。并且,在
+munmap
中是一页一页地释放,而不是直接传参length全部释放,这一点也很重要。因为我们的lazy allocation很可能导致va~va+length
这一区间内只是部分页被映射,部分页没有。如果直接传参length释放,那么在遇到第一页未被映射的时候,filewrite
就会终止,该页之后的页就没有被写回文件的机会了。所以结论是,在正确实现的
+munmap
中遇到未映射的页会自动跳过,什么也不会发生。代码
数据结构
-
// in param.h
// in proc.h
struct filemap{
uint isused;//对象池思想。该filemap是否正在被使用
uint64 va;//该文件的起始内存页地址
uint64 okva;//该文件的起始未被读入部分对应的内存地址
struct file* file;//文件
size_t length;//需要映射到内存的长度
int flags;//MAP_SHARED OR MAP_PRIVATE
int prot;//PROT_READ OR PROT_WRITE
uint64 offset;//va相对于file开头的offset
};
// Per-process state
struct proc {
struct filemap filemaps[NFILEMAP];
};这玩意的文件结点实际上是在创建console时整的,并且其有一个特殊的文件类型“STATS”。我们可以进一步追溯到kernel中的
-main.c
:+
void main()
{
if(cpuid() == 0){
statsinit();mmap
具体系统调用注册过程略。
+-
// in sysproc.c
uint64
sys_mmap(void){
uint64 addr;
int length,prot,flags,offset;
struct file* file;
if(argaddr(0,&addr) < 0 || argint(1,&length) < 0 || argint(2,&prot) < 0 || argint(3,&flags) < 0 || argfd(4,0,&file) ||argint(5,&offset) < 0)
return -1;
return (uint64)mmap((void*)addr,(size_t)length,prot,flags,file,(uint)offset);
}+
void
statsinit(void)
{
initlock(&stats.lock, "stats");
devsw[STATS].read = statsread;
devsw[STATS].write = statswrite;
}-
// 映射file从offset开始长度为length的内容到内存中,返回内存中的文件内容起始地址
void* mmap(void* address,size_t length,int prot,int flags,struct file* file,uint64 offset){
// mmap的prot权限必须与file的权限对应,不能file只读但是mmap却可写且shared
if((prot&PROT_WRITE) != 0&&flags == MAP_SHARED &&file->writable == 0)
return (void*)ERRORADDR;
struct proc* p = myproc();
uint64 va = 0;
int i=0;
//找到filemap池中第一个空闲的filemap
for(i=0;i<NFILEMAP;i++){
if(!p->filemaps[i].isused){
// 获取va,也即真正的address
va = p->sz;
p->sz += length;
// 其实这里用一个memcpy会更加优雅,可惜我忘记了()
p->filemaps[i].isused = 1;
p->filemaps[i].va = va;
p->filemaps[i].okva = va;
p->filemaps[i].length = length;
p->filemaps[i].prot = prot;
p->filemaps[i].flags = flags;
p->filemaps[i].file = file;
p->filemaps[i].file->off = offset;
p->filemaps[i].offset = offset;
// 增加文件引用数
filedup(file);
break;
}
}
if(va == 0) return (void*)ERRORADDR;
// return start of free memory
uint64 start_va = PGROUNDUP(va);
// 先读入处于proc已申请的内存页区域(也即没有内存对齐情况下)
uint64 off = start_va - va;
if(off < PGSIZE){
fileread(file,va,off);
file->off += off;
p->filemaps[i].okva = va+off;
}
return (void*)va;
}可以看到,它给这个
-STATS
文件类型注册了这两个函数。当我们调用read和write时,实际上就是在调用这俩玩意。我们可以看下这两个handler都干了啥。+
static struct {
struct spinlock lock;
char buf[BUFSZ];
int sz;
int off;
} stats;
int statsread(int user_dst, uint64 dst, int n) {
int m;
acquire(&stats.lock);
if(stats.sz == 0) {
stats.sz = statslock(stats.buf, BUFSZ); // 把信息copy进自己的缓冲区里
}
m = stats.sz - stats.off;
if (m > 0) { // 如果有新东西,就copy到用户缓冲区里
if(m > n) m = n;
if(either_copyout(user_dst, dst, stats.buf+stats.off, m) != -1) {
stats.off += m;
}
} else {
m = -1;
stats.sz = 0;
stats.off = 0;
}
release(&stats.lock);
return m;
}usertrap
错的
-
} else if(r_scause() == 13 || r_scause() == 15){
uint64 va = r_stval();
for(int i=0;i<NFILEMAP;i++){
// 找到va对应的filemap
if(p->filemaps[i].isused&&va>=p->filemaps[i].va
&& va<p->filemaps[i].va+p->filemaps[i].length){
// 说明本来就不应该写
if(r_scause() == 15 && ((p->filemaps[i].prot)&PROT_WRITE) == 0){
p->killed = 1;
break;
}
//说明地址不在文件范围内
if(p->filemaps[i].va+p->filemaps[i].file->ip->size <= va){
p->killed = 1;
break;
}
// 能进到这里来的都是产生了缺页中断,也就是说va对应文件数据不存在
// 我们需要维护一个okva,表示从filemaps.va到okva这段地址已经加载了文件
// 这样一来,我们这里就只需加载okva~va地址对应的文件了
// file结构体自带的off成员会由于fileread而自动增长到对应位置,所以文件可以自然地读写
uint64 start_va = p->filemaps[i].okva;// okva一定是page-align的
// 加载文件内容
while(start_va <= va){
char* mem = kalloc();
if(mem == 0){
p->killed = 1;
break;
}
memset(mem, 0, PGSIZE);
int flag = PTE_X|PTE_R|PTE_U;
if(((p->filemaps[i].prot)&PROT_WRITE) != 0){
flag |= PTE_W;
}
if(mappages(p->pagetable, start_va, PGSIZE, (uint64)mem, flag) != 0){
p->killed = 1;
kfree(mem);
break;
}
// 读入文件内容
fileread(p->filemaps[i].file,start_va,PGSIZE);
start_va += PGSIZE;
}
p->filemaps[i].okva = start_va;
break;
}
}
}+
int statswrite(int user_src, uint64 src, int n) { // WARNING: READ ONLY!!!
return -1;
}对的
-
} else if(r_scause() == 13 || r_scause() == 15){
uint64 va = r_stval();
for(int i=0;i<NFILEMAP;i++){
if(p->filemaps[i].isused&&va>=p->filemaps[i].va && va<p->filemaps[i].va+p->filemaps[i].length){
if(r_scause() == 15 && ((p->filemaps[i].prot)&PROT_WRITE) == 0){
// 说明本来就不应该写
p->killed = 1;
break;
}
if(p->filemaps[i].va+p->filemaps[i].file->ip->size <= va){
//说明地址不在文件范围内
p->killed = 1;
break;
}
uint64 start_va = PGROUNDDOWN(va);
char* mem = kalloc();
if(mem == 0){
p->killed = 1;
break;
}
memset(mem, 0, PGSIZE);
int flag = PTE_X|PTE_R|PTE_U;
if(((p->filemaps[i].prot)&PROT_WRITE) != 0){
flag |= PTE_W;
}
if(mappages(p->pagetable, start_va, PGSIZE, (uint64)mem, flag) != 0){
p->killed = 1;
kfree(mem);
break;
}
readi(p->filemaps[i].file->ip,0,(uint64)mem,va-p->filemaps[i].va+p->filemaps[i].offset,PGSIZE);
break;
}
}
}可以看到其本质就是把
-statslock
返回的东西copy到用户空间了。我们来结合最后的输出效果看看statslock
的具体实现:-
+
int statslock(char *buf, int sz) {
int n;
int tot = 0;
int found = 0;
acquire(&lock_locks);
n = snprintf(buf, sz, "--- lock kmem/bcache stats\n");
for(int i = 0; i < NLOCK; i++) {
if(locks[i] == 0) break;
if(strncmp(locks[i]->name, "bcache", strlen("bcache")) == 0 ||
strncmp(locks[i]->name, "kmem", strlen("kmem")) == 0) {
tot += locks[i]->nts; // 记入->nts计数
/*
snprint_lock: lock: %s: #fetch-and-add %d #acquire() %d\n
*/
n += snprint_lock(buf +n, sz-n, locks[i]);
found += 1;
}
}
// Require at least two locks name after kmem/bcache.
if (found < 2) {
tot = -1;
}
// 简单粗暴地计算前五多争用的进程
n += snprintf(buf+n, sz-n, "--- top 5 contended locks:\n");
int last = 100000000;
// stupid way to compute top 5 contended locks
for(int t = 0; t < 5; t++) {
int top = 0;
for(int i = 0; i < NLOCK; i++) {
if(locks[i] == 0)
break;
if(locks[i]->nts > locks[top]->nts && locks[i]->nts < last) {
top = i;
}
}
/*
snprint_lock: lock: %s: #fetch-and-add %d #acquire() %d\n
*/
n += snprint_lock(buf+n, sz-n, locks[top]);
last = locks[top]->nts;
}
n += snprintf(buf+n, sz-n, "tot= %d\n", tot);
release(&lock_locks);
return n;
}munmap
错的
-
uint64 min(uint64 a,uint64 b){return a>b?b:a;}
// 释放文件映射以address为起始地址,length为长度这个范围内的内存地址空间
int munmap(void* address,size_t length){
struct proc* p = myproc();
uint64 va = (uint64)address;
// 找到对应的filemap
for(int i=0;i<NFILEMAP;i++){
if(p->filemaps[i].isused&&p->filemaps[i].va<=va&&p->filemaps[i].va+length>va){
// 开始释放的内存地址
uint64 start_va;
if(va == p->filemaps[i].va)
start_va = PGROUNDUP(p->filemaps[i].va);
else
start_va = PGROUNDDOWN(va);
// 结束释放的内存地址
uint64 bounder = p->filemaps[i].va + min(p->filemaps[i].file->ip->size,length);
//file的off在trap中用于表示文件已加载的位置
//在这里需要用off进行filewrite,所以需要对原本在usertrap用于记录加载位置的off进行手动保存
uint64 tmp_off = p->filemaps[i].file->off;
p->filemaps[i].file->off = p->filemaps[i].offset+va-p->filemaps[i].va;
//释放已经申请的页表项、内存,并且看看是不是需要写回
while(start_va < bounder && start_va < p->filemaps[i].okva){
if(p->filemaps[i].flags == MAP_SHARED){
//写回
filewrite(p->filemaps[i].file,start_va,PGSIZE);
}
uvmunmap(p->pagetable,start_va,1,1);
start_va += PGSIZE;
}
//修改filemap结构体的起始地址va和长度,offset也要变,因为他记录va对应的是文件哪个位置
if(va == p->filemaps[i].va){
//释放的是头几页
p->filemaps[i].offset += length;
p->filemaps[i].va = va+length;
p->filemaps[i].length -= length;
}else {
//释放的是尾几页
p->filemaps[i].length -= p->filemaps[i].length - va;
}
p->filemaps[i].file->off = tmp_off;
// 检验map的合理性
if(p->filemaps[i].length == 0 || p->filemaps[i].va >= p->filemaps[i].va+length
|| p->filemaps[i].file->off > p->filemaps[i].file->ip->size){
p->filemaps[i].isused = 0;//释放
// 注意!!!!这句话对我的错误代码来说非常重要
p->filemaps[i].file->off = 0;
fileclose(p->filemaps[i].file);
}
}
}
return 0;
}可以看到其争用本质计算是通过
-spinlock::nts
字段记录。我们来看看这玩意的引用:+
void initlock(struct spinlock *lk, char *name) {
lk->nts = 0;
}
void acquire(struct spinlock *lk) {
...
while(__sync_lock_test_and_set(&lk->locked, 1) != 0) {
__sync_fetch_and_add(&(lk->nts), 1);
;
}对的
-
uint64 min(uint64 a,uint64 b){return a>b?b:a;}
int munmap(void* address,size_t length){
struct proc* p = myproc();
uint64 va = (uint64)address;
for(int i=0;i<NFILEMAP;i++){
if(p->filemaps[i].isused&&p->filemaps[i].va<=va&&p->filemaps[i].va+length>va){
uint64 start_va;
if(va == p->filemaps[i].va)
start_va = PGROUNDUP(p->filemaps[i].va);
else
start_va = PGROUNDDOWN(va);
uint64 bounder = p->filemaps[i].va + min(p->filemaps[i].file->ip->size,length);
//在这里需要用off进行读写,所以需要对原本的加载处off手动保存
uint64 tmp_off = p->filemaps[i].file->off;
p->filemaps[i].file->off = p->filemaps[i].offset+va-p->filemaps[i].va;
//释放已经申请的页表项、内存,并且看看是不是需要写回
while(start_va < bounder){
if(p->filemaps[i].flags == MAP_SHARED){
//写回
filewrite(p->filemaps[i].file,start_va,PGSIZE);
}
uvmunmap(p->pagetable,start_va,1,1);
start_va += PGSIZE;
}
//修改filemap结构体的起始地址va和长度,offset也要变,因为他记录va对应的是文件哪个位置
if(va == p->filemaps[i].va){
//释放的是头几页
p->filemaps[i].offset += length;
p->filemaps[i].va = va+length;
p->filemaps[i].length -= length;
}else {
//释放的是尾几页
p->filemaps[i].length -= p->filemaps[i].length - va;
}
// 检验map的合理性
if(p->filemaps[i].length == 0 || p->filemaps[i].va >= p->filemaps[i].va+length
|| p->filemaps[i].file->off > p->filemaps[i].file->ip->size){
p->filemaps[i].isused = 0;//释放
fileclose(p->filemaps[i].file);
}
p->filemaps[i].file->off = tmp_off;
}
}
return 0;
}很好,逻辑很简单,就是记录acquire时等待的次数,非常简单粗暴(((
-总的来说这个思路还是挺酷的,而且这个“一切皆文件”的思想再次震撼了我,一个小小的xv6确实能做到那么多。
+exit和fork
exit
+ +
// 关闭map-file
for(int i=0;i<NFILEMAP;i++){
if(p->filemaps[i].isused){
munmap((void*)(p->filemaps[i].va),p->filemaps[i].length);
}
}fork
+ +
for(int i=0;i<NFILEMAP;i++){
np->filemaps[i].isused = p->filemaps[i].isused;
np->filemaps[i].va = p->filemaps[i].va;
np->filemaps[i].okva = p->filemaps[i].okva;
np->filemaps[i].file = p->filemaps[i].file;
np->filemaps[i].length = p->filemaps[i].length;
np->filemaps[i].flags = p->filemaps[i].flags;
np->filemaps[i].offset = p->filemaps[i].offset;
np->filemaps[i].prot = p->filemaps[i].prot;
if(np->filemaps[i].file)
filedup(np->filemaps[i].file);
}修改uvmcopy和uvmunmap
]]>
// in uvmunmap()
if((*pte & PTE_V) == 0){
*pte = 0;
continue;
}
// in uvmcopy()
if((*pte & PTE_V) == 0)
//panic("uvmcopy: page not present");
continue;
实验入口
主要参考文章
lseek()函数:用于移动打开文件的指针
linux系统调用之write源码解析(基于linux0.11)
get_fs_bytes解析
VIM与系统剪贴板的复制粘贴
操作系统实验六 信号量的实现和应用(哈工大李治军)
哈工大操作系统实验6 信号量的实现 pc.c 编译时报错 对‘sem_open‘未定义的引用
Linux 文件编程 open函数
哈工大-操作系统-HitOSlab-李治军-实验5-信号量的实现和应用
--参考文章
-Linux进程间通信(六):共享内存 shmget()、shmat()、shmdt()、shmctl()
- +总耗时:120h 约27天
+部分地方的翻译和表格来源参考:xv6指导书翻译
+部分文本来自:操作系统实验指导书 - 2023秋季 | 哈工大(深圳)
+实验官网:6.S081
+代码以github为准,此处记录的有些小瑕疵
+笔记的结构【以第一章Operating system interface为例】:
+
顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
-注:共享内存并未提供同步机制,所以我们需要用信号量来实现同步。
-Linux提供了一组接口用于使用共享内存,它们声明在头文件 sys/shm.h 中。
-程序先通过调用shmget()函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget()函数的返回值)。
-int shmget(key_t key, size_t size, int shmflg); |
key为共享内存段名字,size为大小,shmflg是权限标志
-注:
-① key:非0整数,共享内存段的命名
-② shmflag:作用与open函数的mode参数一样,比如IPC_CREAT,或连接
-共享内存的权限标志与文件的读写权限一样,举例来说,0644表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存
-③ return:成功时返回一个与key相关的共享内存标识符(非负整数)。调用失败返回-1
-不相关的进程可以返回值(共享内存标识符)访问同一共享内存。
-第一次创建完共享内存时,它还不能被任何进程访问,需要shmat启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。
-void *shmat(int shm_id, const void *shm_addr, int shmflg); |
① shm_id:共享内存标识符
-② shm_addr:指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址
-③ shm_flg:一组标志位,通常为0
-④ return:成功时返回一个指向共享内存第一个字节的指针,失败返回-1
-用于将共享内存从当前进程中分离,使该共享内存对当前进程不再可用。
-int shmdt(const void *shmaddr); |
① shmaddr:shmat返回的共享内存指针
-② return:成功0,失败1
-用来控制共享内存
-int shmctl(int shm_id, int command, struct shmid_ds *buf); |
① shm_id:共享内存标识符
-② command:要采取的操作,它可以取下面的三个值 :
-③ buf:结构指针
-shmid_ds结构 至少包括以下成员:
-struct shmid_ds |
-本项实验在 Ubuntu 下完成,与信号量实验中的
-pc.c
的功能要求基本一致,仅有两点不同:-
-- 不用文件做缓冲区,而是使用共享内存;
-- 生产者和消费者分别是不同的程序。生产者是 producer.c,消费者是 consumer.c。两个程序都是单进程的,通过信号量和缓冲区进行通信。
-Linux 下,可以通过
+shmget()
和shmat()
两个系统调用使用共享内存。Operating system interface
Operating system oganization
Page tables
Traps and system calls
Interrupts and device drivers
Locking
Scheduling
File system
其他的对实验未涉及的思考
]]>
xv6中,每个CPU中的scheduler都有一个专用的线程。这里线程的概念是,有自己的栈,有自己的寄存器状态。
+当发生时钟中断时,当前进程调用yield,yield再通过swtch切换到scheduler线程。scheduler线程会通过swtch跳转到另外一个进程去执行。当另外一个进程发生时钟中断,又会通过yield回到scheduler,scheduler再调度原进程继续执行,如此周而复始。
++-Linux的调度原理也差不多类似这样。每个CPU都有一个调度类为SCHED_CLASS_IDLE的IDLE进程,IDLE进程体大概就是间歇不断地执行__schedule()函数,CPU空闲时就会不断执行IDLE线程。
+而当有新的任务产生时(或任务被唤醒。可以从此看出task new和task wakeup的共通点,可以联想到竞赛中对该消息的处理方法),它首先通过调度类对应的select_cpu选择一个合适的(可以被抢占&&在该task对应的cpumask中)的cpu,迁移到cpu对应的rq;目标cpu通过IDLE进程体或者中断返回时检查到了NEED_SCHEDULE标记位,从而调用schedule函数pick新任务,然后进行context_switch切换到目标线程。如此周而复始。
直接上代码。感觉比文件操作简单多了2333
-consumer.c
-
|
producer.c
-
|
编译运行指令
-gcc -o producer producer.c -pthread |
运行结果c.txt(仅展示部分)
-27696 : 000 |
下面就来讲讲这个所谓的“线程”以及xv6的上下文切换是怎么实现的。
+上下文切换的操作对象是上下文,因而首先了解一下上下文的结构。各种寄存器的状态即是上下文context。xv6中的context定义如下:
+struct context { |
上下文切换需要修改栈和pc,context中确实有sp寄存器,但是没有pc寄存器,这主要还是因为当swtch返回时,会回到ra所指向的地方,所以仅保存ra就足够了。
+上下文的切换是通过swtch实现的。
+void swtch(struct context*, struct context*); |
swtch会把当前进程的上下文保存在第一个context中,再切换到第二个context保存的上下文,具体实现就是写读保存寄存器:
+# in kernel/swtch.S |
-+进程之间可以通过页共享进行通信,被共享的页叫做共享内存,结构如下图所示:
-+
sched
在sleep、yield和wakeup中,都会通过sched中的swtch进入scheduler线程。
+-
void
sched(void)
{
int intena;
struct proc *p = myproc();
if(!holding(&p->lock))
panic("sched p->lock");
if(mycpu()->noff != 1)
panic("sched locks");
if(p->state == RUNNING)
panic("sched running");
if(intr_get()) // 当持有锁时一定为关中断状态
panic("sched interruptible");
intena = mycpu()->intena;
swtch(&p->context, &mycpu()->context);
mycpu()->intena = intena;
}本部分实验内容是在 Linux 0.11 上实现上述页面共享,并将上一部分实现的 producer.c 和 consumer.c 移植过来,验证页面共享的有效性。
-
cpu中存储着的是scheduler线程的context。因而,这样就可以保存当前进程的context,读取scheduler线程的context,然后转换到scheduler的context执行了。
--具体要求在
-mm/shm.c
中实现shmget()
和shmat()
两个系统调用。它们能支持producer.c
和consumer.c
的运行即可,不需要完整地实现 POSIX 所规定的功能。-
-- shmget()
-- -
int shmget(key_t key, size_t size, int shmflg);-
shmget()
会新建/打开一页内存,并返回该页共享内存的 shmid(该块共享内存在操作系统内部的 id)。所有使用同一块共享内存的进程都要使用相同的 key 参数。
-如果 key 所对应的共享内存已经建立,则直接返回
-shmid
。如果 size 超过一页内存的大小,返回-1
,并置errno
为EINVAL
。如果系统无空闲内存,返回 -1,并置errno
为ENOMEM
。-
shmflg
参数可忽略。-
-- shmat()
-- -
void *shmat(int shmid, const void *shmaddr, int shmflg);-
shmat()
会将shmid
指定的共享页面映射到当前进程的虚拟地址空间中,并将其首地址返回。如果
-shmid
非法,返回-1
,并置errno
为EINVAL
。+
shmaddr
和shmflg
参数可忽略。可以发现这里是有个很完美的组合技的。由sched()保存context到process结构体中,再由scheduler()读取process对应的context回归到sched()继续执行,我感觉调度设计这点真是帅的一匹。
思路:
-1.shmget:由其论述,我们可以知道,我们需要建立一个映射表,其中成员为结构体({key_t key,size_t size,unsigned long page}),每次只需查找映射表,如果有对应key则返回下标,如果没有则新建页表,填入映射体,再返回对应下标。
-以下为了图省事,对映射表的实现进行了简化,把key直接当做int类型,作为映射表下标,映射表成员为page,unsigned long。
-2.shmat:
-首先由指导书的提示:
-// 建立线性地址和物理地址的映射 |
我们知道在shmat中,要建立shmget得到的共享物理页面与其虚拟地址的映射,就需要使用这个put_page函数。
-但是put_page函数的参数为页和address。页就是我们的shm_map[key],address=虚拟地址+段基址。那么如何得到虚拟地址呢?
-通过指导书的提示:
-code_base = get_base(current->ldt[1]); |
再结合所学知识,我们可以知道几点:
-① 数据段的基址可由current->ldt[2]给出 ② address=虚拟地址+段基址 ③ 我们需要分配给当前共享内存一段空闲的虚拟地址段
-则该小段空闲数据段的虚拟地址就是我们的return值,address=return+data_base。
-问题就转化成了如何获取一段空闲数据段。
-我们由下图:
-void |
可知,brk指针指向堆区顶部,即空闲堆的起始位置。因而我们可以用这段空间作为我们要的空闲数据段,当前brk即为虚拟地址。
-我们的页有PAGE_SIZE那么大,因而自然也就要用PAGE_SIZE那么大的空闲数据段了。
-解说完毕,以下上代码~
-
|
通过swtch进入scheduler线程后,会继续执行scheduler中swtch的下一个指令,完成下一次调度。
+以上是书本的介绍内容。看到这想必会有很多疑惑,至少有以下两点:
+先从第一点入手。实际上,这个初始化的工作,是在操作系统启动时的main.c
中完成的。
void |
在 Ubuntu 上编写应用程序“pc.c”,解决经典的生产者—消费者问题,完成下面的功能: |
在这之前,创建了第一个进程proc。在这里,每个cpu都调用了scheduler。
+void |
先附上我的代码吧【注:我没做到从缓冲区删除,但其他都完成了】
-
|
每个cpu都在scheduler线程的swtch(&c->context, &p->context);
中,将当前的context,也即scheduler的context存入了mycpu()->context
。随后,CPU中的某个去执行下一个进程,其他的就在scheduler线程的无限循环中等待,直到有别的进程产生。
去执行进程的CPU通过swtch切换上下文,切到了另一个进程中,此时在swtch中保存的ra是scheduler线程的swtch的下一句(因为scheduler->swtch也是个函数调用的过程)。会切到另一个进程的sched的下一句【因为它正是从那边swtch过来的】,或者是那个进程开始执行的地方【下面会提到是forkret】。另一个进程通过sched切换回来的时候,就正会切到ra所指向的位置,也即切到scheduler中的swtch后面。
+这样一来,两个问题就都得到了解答。
+从这,我们也能知道xv6是如何让CPU运转的:scheduler线程是CPU的IDLE状态。无事的时候在scheduler中等待,并且一直监测是否有进程需要执行。有的话,则去执行该进程;该进程又会通过sched切换回scheduler线程,继续等待。这样一来,就完成了进程管理的基本的自动机图像。
+++A process that wants to give up the CPU must do three things:
++
+- acquire its own process lock p->lock, release any other locks it is holding
+- update its own state (p->state)
+- call sched
++
yield
(kernel/proc.c:515) follows this convention, as dosleep
andexit
.+
sched
double-checks those conditions (kernel/proc.c:499-504) and then an implication of those conditions: since a lock is held, interrupts should be disabled.
在上面的描述我们可以看到,sched
和scheduler
联系非常密切,他们俩通过swtch
相互切来切去,并且一直都只在这几行切来切去:
// in scheduler() |
运行效果:
-在两个线程之间进行这种样式化切换的过程有时被称为协程(coroutines)。
+++存在一种情况使得调度程序对
+swtch
的调用没有以sched
结束。一个新进程第一次被调度时,它从forkret
(kernel/proc.c:527)开始。Forkret
是为了释放p->lock
而包装的,要不然,新进程可以从usertrapret
开始。
++考虑调度代码结构的一种方法是,它为每个进程强制维持一个不变性条件的集合,并在这些不变性条件不成立时持有
+p->lock
。其中一个不变性条件是:如果进程是
+RUNNING
状态,计时器中断的yield
必须能够安全地从进程中切换出去;这意味着CPU寄存器必须保存进程的寄存器值(即swtch
没有将它们移动到context
中),并且c->proc
必须指向进程。另一个不变性条件是:如果进程是RUNNABLE
状态,空闲CPU的调度程序必须安全地运行它;这意味着p->context
必须保存进程的寄存器(即,它们实际上不在实际寄存器中),没有CPU在进程的内核栈上执行,并且没有CPU的c->proc
引用进程。维护上述不变性条件是xv6经常在一个线程中获取p->lock并在另一个线程中释放它的原因,在保持
+p->lock
时,这些属性通常不成立。例如在
+yield
中获取并在scheduler
中释放。一旦yield
开始修改一个RUNNING
进程的状态为RUNNABLE
,锁必须保持被持有状态,直到不变量恢复:最早的正确释放点是scheduler
(在其自身栈上运行)清除c->proc
之后。类似地,一旦scheduler
开始将RUNNABLE
进程转换为RUNNING
,在内核线程完全运行之前(在swtch
之后,例如在yield
中)绝不能释放锁。+
p->lock
还保护其他东西:exit
和wait
之间的相互作用,避免丢失wakeup
的机制(参见第7.5节),以及避免一个进程退出和其他进程读写其状态之间的争用(例如,exit
系统调用查看p->pid
并设置p->killed
(kernel/proc.c:611))。为了清晰起见,也许为了性能起见,有必要考虑一下p->lock
的不同功能是否可以拆分。
p->lock在每次scheduler开始的时候获取,swtch到p进程的时候在yield等调用完sched的地方释放。而调用yield时获取的锁,又会在scheduler中释放。
+// Give up the CPU for one scheduling round. |
这部分耗费了我海量时间,主要原因还是因为我没有好好学就直接上手写导致很多地方都因为不清楚而寄了。。。
-先大致讲讲文件读写的原理吧。打开一个文件作为数据流,有一个文件指针,该指针指向的地方就是之后读写开始的地方,读写还有lseek都可以让指针移动。
-再放个各个系统调用的签名。
-@param 文件名 模式 |
// in kernel/proc.c scheduler() |
其中flag的可能取值:
-不得不说,这结构实在是太精妙了。这中间的如此多的复杂过程,就这样成功地被锁保护了起来。
+// Per-CPU state. |
mycpu是通过获取当前cpuid来获取cpu结构的。当前使用的cpuid约定俗成地存在了tp寄存器里。为了让mycpu有效工作,必须确保tp寄存器始终存放的是当前cpu的hartid。
+首先是在操作系统初始化的时候要把cpuid存入tp寄存器。RISC-V规定,mhartid也即cpuid的存放点只能在machine mode被读取。因而这项工作得在start.c
中完成:
// in kernel/start.c |
如果想要多个方式并行,则可以用|连接。【联系一下原理,这大概是用了标志位吧,每个标志只有一位是1】
-这部分踩过的坑:
-① 选择O_CREAT,如果文件已经存在,居然是会报错?【表现为errno=13,还会输出一堆奇怪的东西】
-@param 文件描述符 写入字符串 写入长度 |
在内核态中,编译器被设置为保证不会以其他方式使用tp寄存器。因而初始化之后,内核态中每个CPU的tp寄存器就始终存放着自己的cpuid。
+但这在用户进程是不成立的。因而必须在用户进程进入陷阱的时候做一些工作。
+# in kernel/trampoline.S uservec |
read会读出size个字节然后存进string里面,同时也会移动文件指针向前size个字节。
-@param 文件描述符 写入字符串 写入长度 |
struct trapframe { |
基本同write。
-这部分踩过的坑:
-write(fd,NULL,0) ——合法
-write(fd,NULL,a),a>0 ——寄!
-这还是因为write的具体实现了。
-write里面有个判断
-int sys_write(unsigned int fd,char *buf,int count){ |
必须在trampoline保存用户态中使用的tp值,以及内核态中对应的hartid。
+最后再在返回用户态的时候恢复用户态的tp值以及更新trampoline的tp值。
+// in kernel/trap.c usertrapret() |
而get_fs_byte:
-# in trampoline.S userret |
确实感觉空的话挺危险的【】
-@param 文件描述符 |
注意,更新trampoline的tp值这一步很重要。因为如果在用户态发生的是时钟中断,就会引起yield,可能造成CPU的切换。这时候就需要在返回用户态的时候修改一下trapframe中的tp为当前CPU的tp。这样一来才能保证,在本次时钟中断结束,以及到下一次时钟中断修改CPU这一期间,trapframe中的tp寄存器以及内核态中的tp寄存器都是正确的。
+通过mycpu()
获取cpuid其实是非常脆弱的。因为你可能获取完cpuid,进程就被切到别的CPU去执行了,这就会有一个先检查后执行的竞态条件,具有并发安全隐患。因而,xv6要求使用mycpu()
返回值的这段代码需要关中断,这样就可以避免时钟中断造成的进程切换了。比如说像myproc()
这样:
// Return the current struct proc *, or zero if none. |
这个没啥好说的,记得关就是了
-这方面看linux自带的man文档就行,写得很清楚。
-输入指令:
-man sem_overview |
注意,不同于mycpu()
,使用myproc()
的返回值不需要进行开关中断保护。因为当前进程的指针不论处于哪个CPU都是不变的。
前面我们已经介绍了进程隔离性的基本图像,接下来要讲xv6是如何让进程之间互动的。xv6使用的是经典的sleep and wakeup,也叫序列协调(sequence coordination)或条件同步机制(conditional synchronization mechanisms。下面,将从最基本的自旋锁实现信号量开始,来逐步讲解xv6的sleep and wakeup机制。
+缺点就是自旋太久了,因而我们需要在等待的时候调用yield,直到资源生产出来之后再继续执行。
+++Let’s imagine a pair of calls, sleep and wakeup, that work as follows:
++
+- +
+
sleep(chan)
Sleeps on the arbitrary value chan, called the wait channel. Sleep puts the calling process to sleep, releasing the CPU for other work.
+- +
+
wakeup(chan)
Wakes all processes sleeping on chan (if any), causing their sleep calls to return. If no processes are waiting on chan, wakeup does nothing.
+
这样一来,信号量实现就可修改为这样了:
+但是,我们可以注意到,在212-213行这里产生了一个先检查后执行的竞态条件。
+++如果消费者进程执行到212-213中间,此时生产者进程已经调用结束,也就是说wakeup并没有唤醒任何消费者进程。消费者进程就会一直在sleep中没人唤醒,除非生产者进程再执行一次。这样就会造成lost wake-up 这个问题。
+
所以,我们可以选择把这个竞态条件也放入s->lock这个锁区域保护。
+但是这样一来又会产生死锁问题。因而,我们可以尝试着修改sleep和wakeup的接口定义。
+++We’ll fix the preceding scheme by changing sleep’s interface:
+The caller must pass the condition lock to sleep so it can release the lock after the calling process is marked as asleep and waiting on the sleep channel. The lock will force a concurrent V to wait until P has finished putting itself to sleep, so that the wakeup will find the sleeping consumer and wake it up. Once the consumer is awake again sleep reacquires the lock before returning.
+也即在sleep中:
++
sleep(s,&s->lock){
// do something
release(&s->lock);
//wait until wakeup
acquire(&s->lock);
return;
}
这样一来,信号量就可以完美实现了:
+++注:严格地说,
+wakeup
只需跟在acquire
之后就足够了(也就是说,可以在release
之后调用wakeup
)【想了一下,有一说一确实,放在release前后都不影响】
+
++原始Unix内核的
+sleep
只是禁用了中断,这就足够了,因为Unix运行在单CPU系统上。因为xv6在多处理器上运行,所以它为sleep
添加了一个显式锁。
// Atomically release lock and sleep on chan. |
这部分踩过的坑:
-千万注意最后不使用信号量时要释放,使用sem_unlink。不然最后的输出结果会非常诡异。
-以上差不多就是涉及到的需要自己了解的课外知识点了,接下来就需要自己编写程序。
-总体框架就按它给的差不多:
-Producer() |
注意,如果lk为p->lock,那么lk依然会在scheduler线程中被暂时释放。
+// Wake up all processes sleeping on chan. |
有个点挺有趣的,就是它实际上把文件指针也看成一种资源了,因此也需要在同步段对其进行更新。
-printf的stdout也是资源。
-故以上两者都只能在锁内同步段进行更新。
-main函数就照本宣科地用fork建立子进程就行。
-Linux 在 0.11 版还没有实现信号量,Linus 把这件富有挑战的工作留给了你。如果能实现一套山寨版的完全符合 POSIX 规范的信号量,无疑是很有成就感的。但时间暂时不允许我们这么做,所以先弄一套缩水版的类 POSIX 信号量,它的函数原型和标准并不完全相同,而且只包含如下系统调用: |
可以注意到,关于chan
这一变量的取值是非常任意的,仅需取一个约定俗成的值就OK。这里取为了信号量的地址,同时满足了逻辑需求和语义需求。
++Callers of sleep and wakeup can use any mutually convenient number as the channel. Xv6 often uses the address of a kernel data structure involved in the waiting.
+
这里也解释了为什么需要while循环
+++有时,多个进程在同一个通道上睡眠;例如,多个进程读取同一个管道。一个单独的
+wakeup
调用就能把他们全部唤醒。其中一个将首先运行并获取与sleep
一同调用的锁,并且(在管道例子中)读取在管道中等待的任何数据。尽管被唤醒,其他进程将发现没有要读取的数据。从他们的角度来看,醒来是“虚假的”,他们必须再次睡眠。因此,在检查条件的循环中总是调用sleep
。
pipes很显然就是生产者消费者模式的一个例证。
+struct pipe { |
由于不小心写完的实验代码被销毁了,因此差不多参考的是这篇文章【戳这里】,修改了一些地方,构成了我的回忆版代码。
-详见文章,写得很清楚。
-sem_t定义
-/* 定义的信号量数据结构: */ |
int |
|
int |
sem_open
-/* |
一个非常有意思且巧妙的点,就是读写管道等待在不同的chan上,这与上面信号量的例子是不一样的。想想也确实,如果使用同一个管道的话,当唤醒的时候,就会把不论是读还是写的全部进程都唤醒过来,这对性能显然损失较大。
+++The pipe code uses separate sleep channels for reader and writer (pi->nread and pi->nwrite); this might make the system more effificient in the unlikely event that there are lots of readers and writers waiting for the same pipe.
+
+++
Sleep
和wakeup
可用于多种等待。第一章介绍的一个有趣的例子是子进程exit
和父进程wait
之间的交互。xv6记录子进程终止直到
+wait
观察到它的方式是让exit
将调用方置于ZOMBIE
状态,在那里它一直保持到父进程的wait
注意到它,将子进程的状态更改为UNUSED
,复制子进程的exit
状态码,释放子进程,并将子进程ID返回给父进程。如果父进程在子进程之前退出,则父进程将子进程交给
+init
进程,init
进程将永久调用wait
;因此,每个子进程退出后都有一个父进程进行清理。
又是一个生产者消费者模式。只不过此时的chan是父进程,资源是僵尸子进程【草】。由于涉及到进程间的调度切换,因而实现稍微复杂了点。
+为什么需要涉及到进程间的调度呢?子进程设置完僵尸状态后,直接通过函数ret不行吗?答案是不行,因为ret的话就会去到不知道哪的地方【大概率会变成scause=2的情况】,所以这里子进程想要退出,就得做几件事,一是依靠父进程,让父进程杀死子进程,二是把自己设置为一个特殊的状态,使得自己不会被调度从而执行ret指令出错,三是尽快让父进程杀死自己越快越好。综合上述三个原因,exit最终在调度方面的实现方式,就变成了,子进程设置自己为ZOMBIE态->启用调度->父进程杀死ZOMBIE态的子进程。这期间不变性条件的防护,就得依赖于锁,以及sleep和wakeup了。
+void |
sem_wait
-/* |
int |
sem_post
-/* |
其中值得注意的几个点:
+wait
中的sleep
中释放的条件锁是等待进程的p->lock
,这是上面提到的特例。
exit会将自己的所有子进程交付给一直在等待着的init进程:
+for(;;){ |
sem_unlink
-/* |
如果子进程退出,就会通过init的wait释放它们。然后init释放完它们后进入第三个if分支,继续进行循环。
+wakeup1
+++Exit calls a specialized wakeup function, wakeup1, that wakes up only the parent, and only if it is sleeping in wait.
+
// Wake up p if it is sleeping in wait(); used by exit(). |
kill其实做得很温和。它只是会把想鲨的进程的p->killed设置为1,然后如果该进程sleeping,则唤醒它。最后的死亡以及销毁由进程自己来做。
+// Kill the process with the given pid. |
可能这里有一个疑问:调用完exit后,进程会变成ZOMBIE态。谁最终把它释放了呢?其实答案很简单,只有两种:init进程或者是创建它的父进程。
+如果创建它的父进程处于wait中,那么是由父进程把它销毁的,这没什么好说的。但如果创建它的父进程不在wait呢?那么父进程最后也是会调用exit的。父进程调用完exit后,会将其所有子进程过继给init进程。所以,ZOMBIE进程最终还是会迟早被init进程杀死的。
+由这里,可以窥见xv6进程管理的进一步的冰山一角:
+init进程是所有进程的根系进程。它一直处于wait的死循环中,因而可以将需要被杀死的进程杀死。
+可见,wait和exit,实际上就构筑了进程的生命周期的最后一环。
+这种巧妙地将进程生命周期这个大事完全托付给了wait和exit这两个函数的这种结构,实在是非常精妙,太牛了吧。
+++一些XV6的
+sleep
循环不检查p->killed
,因为代码在应该是原子操作的多步系统调用的中间。virtio驱动程序(*kernel/virtio_disk.c*:242)就是一个例子:它不检查p->killed
,因为一个磁盘操作可能是文件系统保持正确状态所需的一组写入操作之一。等待磁盘I/O时被杀死的进程将不会退出,直到它完成当前系统调用并且usertrap
看到killed
标志
++Xv6对
+kill
的支持并不完全令人满意:有一些sleep
循环可能应该检查p->killed
。一个相关的问题是,即使对于检查p->killed
的sleep
循环,sleep
和kill
之间也存在竞争;后者可能会设置p->killed
,并试图在受害者的循环检查p->killed
之后但在调用sleep
之前尝试唤醒受害者。如果出现此问题,受害者将不会注意到p->killed
,直到其等待的条件发生。这可能比正常情况要晚一点(例如,当virtio驱动程序返回受害者正在等待的磁盘块时)或永远不会发生(例如,如果受害者正在等待来自控制台的输入,但用户没有键入任何输入)。
是的,所以这个kill的实现其实是相当玄学的。
+++xv6调度器实现了一个简单的调度策略:它依次运行每个进程。这一策略被称为轮询调度(round robin)。真实的操作系统实施更复杂的策略,例如,允许进程具有优先级。
+
我记得linux0.11用的是时间片轮转+优先级队列完美融合的方法,是真的很牛逼
+++复杂的策略可能会导致意外的交互,例如优先级反转(priority inversion)和航队(convoys)。当低优先级进程和高优先级进程共享一个锁时,可能会发生优先级反转,当低优先级进程持有该锁时,可能会阻止高优先级进程前进。当许多高优先级进程正在等待一个获得共享锁的低优先级进程时,可能会形成一个长的等待进程航队;一旦航队形成,它可以持续很长时间。为了避免此类问题,在复杂的调度器中需要额外的机制。
+
++在
+wakeup
中扫描整个进程列表以查找具有匹配chan
的进程效率低下。一个更好的解决方案是用一个数据结构替换sleep
和wakeup
中的chan
,该数据结构包含在该结构上休眠的进程列表,例如Linux的等待队列。
是的,linux的那个wakeup真的很牛,我现在都还记得当初学到那的时候的震撼。
++++
wakeup
的实现会唤醒在特定通道上等待的所有进程,可能有许多进程在等待该特定通道。操作系统将安排所有这些进程,它们将竞相检查睡眠条件。进程的这种行为有时被称为惊群效应(thundering herd),最好避免。大多数条件变量都有两个用于唤醒的原语:
+signal
用于唤醒一个进程;broadcast
用于唤醒所有等待进程。
++一个实际的操作系统将在固定时间内使用空闲列表找到自由的
+proc
结构体,而不是allocproc
中的线性时间搜索;xv6使用线性扫描是为了简单起见。
++You will implement switching between threads in a user-level threads package, use multiple threads to speed up a program, and implement a barrier.
+
这个introduction看起来还是非常激动人心的,很早就想了解到底线程是怎么实现的了。不过做完发现思想还是很简单的,就是只用切换上下文和栈就行。可以看看提供给的代码。
+++In this exercise you will design the context switch mechanism for a user-level threading system, and then implement it.
+To get you started, your xv6 has two files
+user/uthread.c
anduser/uthread_switch.S
, and a rule in the Makefile to build a uthread program.+
uthread.c
contains most of a user-level threading package, and code for three simple test threads. The threading package is missing some of the code to create a thread and to switch between threads.You will need to add code to
+thread_create()
andthread_schedule()
inuser/uthread.c
, andthread_switch
inuser/uthread_switch.S
.One goal is ensure that when
+thread_schedule()
runs a given thread for the first time, the thread executes the function passed tothread_create()
, on its own stack.Another goal is to ensure that
+thread_switch
saves the registers of the thread being switched away from, restores the registers of the thread being switched to, and returns to the point in the latter thread’s instructions where it last left off. You will have to decide where to save/restore registers; modifyingstruct thread
to hold registers is a good plan.You’ll need to add a call to
+thread_switch
inthread_schedule
; you can pass whatever arguments you need tothread_switch
, but the intent is to switch from threadt
tonext_thread
.
实现的是用户级线程,其栈保存在对应父进程的地址空间中。
+看了一遍它这里面写的题目还是有点抽象的,需要结合着给的代码看,那样就清晰多了。
+首先,要补全的地方有这几个:
+// 1. in thread_schedule() |
这几个函数到时候会被如此调用:
+int |
pc.c
-
|
所以,我们在第一个地方要做的,就是要填入swtch的签名。第二个地方要做的,就是要想办法让该线程一被启动就去执行参数的函数指针。第三个地方要做的,就是要完成上下文的切换。
+所以思路其实是很直观的。我们可以模仿进程管理中用来表示上下文的context,在thread_create
的时候把里面的ra设置为参数的函数指针入口,sp修改为thread结构体中的栈地址。swtch函数则完全把kernel/swtch.S
超过来就行。
++在这个思路中,我们是怎么做到栈的切换的呢?
+每个线程在
+thread_create
的时候,都将自己的context中的sp修改为自己的栈地址。这样一来,在它们被调度的时候,switch会自然而然地从context中读取sp作为之后运行的sp,这样就实现了栈的切换。
我觉得其他方面都不难,最坑最细节的【也是我完全没有想到的……】就是这里:
+// 修改sp为栈顶 |
需要注意,栈顶并不是t->stack
。
通过测试程序:
+int main(){ |
栈是向下增长的,因而,栈顶确实应该是数组的末尾……
+这里完全没有想到,还是吃了基础的亏啊。
+++如果这里将
+t->stack
作为sp,那么运行时会出现非常诡异的现象(打印的是abc三个的thread->state
):+
仅有c【经测试,是仅有最后一个启动的线程】在执行,而ab的state都不是理想中的2,而是很奇怪的值。我确实有想过栈溢出问题,但是马上被我否定了。我完全没有想到是那样错的【悲】
+
Update,验收时学长问为什么这里的uswitch.S为什么无需保存tn这样的寄存器。答案是因为tn是caller-save的,线程这相当于仅仅是执行一个函数,所以只需保存callee-save的寄存器。
+内核的swtch也只保存了这些callee-save的寄存器,也是同一个道理。
+tn寄存器被保存在调用者的栈帧中。感觉也能理解为什么那题作业题说上文进程的现场是由栈保存了。
+struct context { |
这部分踩过的坑:
-在用户态和核心态之间传递参数【这个我没考虑到】
-指针参数传递的是应用程序所在地址空间的逻辑地址, |
void |
这段代码就是在做这个。
-/* 首先将信号量的名称赋值到新建的缓冲区中 */ |
这一段代码值得学习
-一个第一眼看傻掉了的问题
-//sleep函数的签名 |
if (current_thread != next_thread) { /* switch threads? */ |
如果队列为空的时候,传入sleep_on的是不是NULL呢?
-其实这个本质上是type* p=NULL,&p是不是NULL的问题。虽然知道不是,但还是写个程序测试一下:
-
|
sem_post签名与实现矛盾
-wake_up() 的功能是唤醒链表上睡眠的所有进程。 |
全部照搬kernel/swtch.S
,没什么好说的
++In this assignment you’ll implement a barrier: a point in an application at which all participating threads must wait until all other participating threads reach that point too.
+
直接上代码,还是比较简单的
+static void |
以上都是指导书的内容。这个“所有”和“一个”的用意我不大明白。也许唤醒所有进程,其中一个抢到了锁,其他的全睡了,这个也被认为是唤醒其中一个吧()
+++]]>Update
+关于pthread_cond的实现,也是使用了条件变量的思想,这个值得以后有时间了解一下。
+
++懂了!VMware/KVM/Docker原来是这么回事儿这篇文章对虚拟化、虚拟机技术讲解很到位,写得通俗易懂,非常值得一看
+KVM 的「基于内核的虚拟机」是什么意思?这篇文章对QEMU-KVM架构进行了详细的介绍。还有这篇文章对应的知乎问题下面的高赞回答有机会也可以去看看。
+QEMU/KVM原理概述这篇文章前面的原理和上面那个差不多,后面有使用kvm做一个精简内核的实例,有兴趣/有精力/有需要可以看看。
+ + +
以前只是知道,xv6是运行在qemu提供的虚拟环境之上的。qemu是什么,怎么虚拟的,虚拟机和宿主机是怎么交互的,这些一概不通。今天心血来潮想研究下qemu,虚拟机啥的到底是什么玩意,虽然看得有些猪脑过载,但还是写下一些个人的整理。
+在了解qemu之前,可以先了解一下虚拟化的思想。
+++虚拟化的主要思想是,通过分层将底层的复杂、难用的资源虚拟抽象成简单、易用的资源,提供给上层使用。
+本质上,计算机的发展过程也是虚拟化不断发展的过程,底层的资源或者通过空间的分割,或者通过时间的分割,将下层的资源通过一种简单易用的方式转换成另一种资源,提供给上层使用。
+虚拟化可分为以下几方面:
++
+- CPU抽象:机器码、汇编语言到C语言、再到高级语言的不断虚拟的过程
+- 存储抽象:操作系统通过文件和目录抽象
+- 网络抽象:TCP/IP协议栈模型将网卡设备中传递的二进制数据,经过网络层、传输层的抽象后,为应用程序提供了便捷的网络包处理接口,而无需关心底层的IP路由、分片等细节
+- 进程抽象:操作系统通过进程抽象为不同的应用程序提供了安全隔离的执行环境,并且有着独立的CPU和内存等资源
+
虚拟化的思想实际上就是我以前一直称为“抽象”的思想,以接口的形式逐层向上服务。
+++虚拟机的核心能力在于提供一个执行环境(隐藏底层细节),并在其中完成用户的指定任务。
+
++虚拟机有多种不同的形式,包括提供指令执行环境的进程、模拟器和高级语言虚拟机,或者是提供一个完整的系统环境的系统虚拟机。
+
进程实际上就是一种虚拟机。
+++进程可以看作是一组资源的集合,有自己独立的进程地址空间以及独立的CPU和寄存器,执行程序员编写的指令,完成一定的任务。
+操作系统可以创建多个进程,每一个进程都可以看成一个独立的虚拟机,它们在执行指令、访问内存的时候并不会相互影响影响。
+
++通过系统虚拟化技术,能够在单个的宿主机硬件平台上运行多个虚拟机,每个虚拟机都有着完整的虚拟机硬件,如虚拟的CPU、内存、虚拟的外设等,并且虚拟机之间能够实现完整的隔离。
+在系统虚拟化中,管理全局物理资源的软件叫作虚拟机监控器(Virtual Machine Monitor,VMM),VMM之于虚拟机就如同操作系统之于进程,VMM利用时分复用或者空分复用的办法将硬件资源在各个虚拟机之间进行分配。
+
可以看到,qemu就是一种虚拟机。它可以模拟虚拟机硬件,为操作系统提供虚拟硬件环境,从而能够让不同的操作系统能够在不同主机硬件上执行。
+++其对于虚拟化技术的优化,以及发展的前因后果,具体可以看懂了!VMware/KVM/Docker原来是这么回事儿这篇文章。
+概括来讲,大致有以下几个要点:
+
一个典型的做法是——陷阱 & 模拟
技术
什么意思?简单来说就是正常情况下直接把虚拟机中的代码指令放到物理的CPU上去执行,一旦执行到一些敏感指令,就触发异常,控制流程交给VMM,由VMM来进行对应的处理,以此来营造出一个虚拟的计算机环境。
+x86架构使得上述做法用不了了。因为它引入了四种权限
+全虚拟化
+VMware的二进制翻译技术、QEMU的模拟指令集
+半虚拟化
+硬件辅助虚拟化
+硬件辅助虚拟化细节较为复杂,简单来说,新一代CPU在原先的Ring0-Ring3四种工作状态之下,再引入了一个叫工作模式的概念,有VMX root operation
和VMX non-root operation
两种模式,每种模式都具有完整的Ring0-Ring3四种工作状态,前者是VMM运行的模式,后者是虚拟机中的OS运行的模式。
qemu-kvm架构正是借助于此实现的。
+kvm就是借助硬件辅助虚拟化诞生的。可以把kvm看作是一堆系统调用。
++ ++KVM本身是一个内核模块,它导出了一系列的接口到用户空间,用户态程序可以使用这些接口创建虚拟机。
+具体而言,KVM 可帮助您将 Linux 转变为虚拟机监控程序,使主机计算机能够运行多个隔离的虚拟环境,即虚拟客户机或虚拟机(VM)。【也即,虚拟机—进程,KVM—操作系统】
+
++在虚拟化底层技术上,KVM和VMware后续版本一样,都是基于硬件辅助虚拟化实现。不同的是VMware作为独立的第三方软件可以安装在Linux、Windows、MacOS等多种不同的操作系统之上,而KVM作为一项虚拟化技术已经集成到Linux内核之中,可以认为Linux内核本身就是一个HyperVisor,这也是KVM名字的含义,因此该技术只能在Linux服务器上使用。
+
KVM本身基于硬件辅助虚拟化,仅仅实现CPU和内存的虚拟化,但一台计算机不仅仅有CPU和内存,还需要各种各样的I/O设备,不过KVM不负责这些。这个时候,QEMU就和KVM搭上了线,经过改造后的QEMU,负责外部设备的虚拟,KVM负责底层执行引擎和内存的虚拟,两者彼此互补,成为新一代云计算虚拟化方案的宠儿。
+KVM只负责最核心的CPU虚拟化和内存虚拟化部分;QEMU作为其用户态组件,负责完成大量外设的模拟。
+++VMX root是宿主机模式,此时CPU在运行包括QEMU在内的普通进程和宿主机的操作系统内核;
+VMX non-root是虚拟机模式,此时CPU在运行虚拟机中的用户程序和操作系统代码。
+
也就是说,虚拟机的程序,包括用户程序和内核程序,都运行在non-root模式。宿主机的所有程序,包括用户程序【包括qemu】和内核程序【包括kvm】,都运行在root模式。
+上面说到,qemu负责的是大量外设的模拟。它具体要做以下几件事:
+++初始化虚拟机:
++
+- +
创建模拟的芯片组
+- +
创建CPU线程来表示虚拟机的CPU
+QEMU在初始化虚拟机的CPU线程时,首先设置好相应的虚拟CPU寄存器的值,然后调用KVM的接口将虚拟机运行起来,这样CPU线程就会被调度在物理CPU上执行虚拟机的代码。
+- +
在QEMU的虚拟地址空间中分配空间作为虚拟机的物理地址
+- +
根据用户在命令行指定的设备为虚拟机创建对应的虚拟设备【如各种IO设备】
+虚拟机运行时:
++
+- +
监听多种事件
+包括虚拟机对设备的I/O访问、用户对虚拟机管理界面、虚拟设备对应的宿主机上的一些I/O事件(比如虚拟机网络数据的接收)等
+- +
调用函数处理
+
可以看到,qemu确实利用了宿主机的各种资源,提供了一个很完美的硬件环境。其资源对应关系为:
+虚拟机的CPU——宿主机的一个线程
+虚拟机的物理地址——qemu在宿主机的虚拟地址
+虚拟机对硬件设备的访问 —→ 对qemu的访问
+它大概做了两件事:
+给qemu提供运行时的参数
+通过“/dev/kvm”设备,比如CPU个数、内存布局、运行等。
+截获VM Exit事件【下面会讲,用来完成虚拟机和硬件环境的交互】并进行处理。
+CPU——QEMU进程中的一个线程
+通过QEMU和KVM的相互协作,虚拟机的线程会被宿主机操作系统正常调度,直接执行虚拟机中的代码
+物理地址——QEMU进程中的虚拟地址
+设备——QEMU实现
+在运行过程中,虚拟机操作系统通过设备的I/O端口(Port IO、PIO)或者MMIO(Memory Mapped I/O)进行交互,KVM会截获这个请求【也即VM Exit,下面会讲】,大多数时候KVM会将请求分发到用户空间的QEMU进程中,由QEMU处理这些I/O请求
+虚拟机肯定是会与它的硬件环境进行交互的,它的硬件环境也就是QEMU—KVM。
+虚拟机的用户程序和内核程序都是直接由宿主机的操作系统正常调度,我们可以将其看作虚拟态。QEMU—KVM可以看作是宿主机的进程,我们可以将其看作宿主态。因而,当虚拟机一些事情希望由QEMU—KVM来做,我们就需要从虚拟态转移到宿主态。
+听起来有没有感觉很耳熟?是的,“从用户态陷入内核态”,跟这个的原理是一样的。
+因而,虚拟机与硬件环境交互,实际上是虚拟态和宿主态状态的转换,如下图:
+VM Exit
+当虚拟机中的代码是敏感指令或者说满足了一定的退出条件时,CPU会从虚拟态退出到KVM,这叫作VM Exit。
+这就像在用户态执行指令陷入内核一样。
+VM Exit首先陷入到KVM中进行处理,如果KVM无法处理,比如说虚拟机写了设备的寄存器地址,那么KVM会将这个写操作分派到QEMU中进行处理。
+VM Entry
+当KVM或者QEMU处好了退出事件之后,又可以将CPU置于虚拟态以运行虚拟机代码,这叫作VM Entry。
+++QEMU在初始化的时候会通过
+mmap
分配虚拟内存空间作为虚拟机的物理内存,【感觉思路打开,物理内存与文件对应了起来】QEMU在不断更新内存布局的过程中会持续调用KVM接口通知内核KVM模块虚拟机的内存分布。
虚拟机在运行过程中,首先需要将虚拟机的虚拟地址(Guest Virtual Address,GVA)转换成虚拟机的物理地址(Guest Physical Address,GPA),然后将虚拟机的物理地址转换成宿主机的虚拟地址(Host Virtual Address,HVA),最终转换成宿主机的物理地址(Host Physical Address,HPA)。
+整个寻址过程由硬件实现,具体实现方式为扩展页表(Extended Page Table,EPT)。
+在支持EPT的环境中,虚拟机在第一次访问内存的时候就会陷入到KVM,KVM会逐渐建立起所谓的EPT页面【lazy思想贯穿始终,还是该叫自适应?】。这样虚拟机的虚拟CPU在后面访问虚拟机虚拟内存地址的时候,首先会被转换为虚拟机物理地址,接着会查找EPT页表,然后得到宿主机物理地址。【有种TLB的感觉】
+设备模拟的本质是要为虚拟机提供一个与物理设备接口完全一致的虚拟接口。
+虚拟机中的操作系统与设备进行的数据交互或者由QEMU和(或)KVM完成,或者由宿主机上对应的后端设备完成。
+QEMU在初始化过程中会创建好模拟芯片组和必要的模拟设备,包括南北桥芯片、PCI根总线、ISA根总线等总线系统,以及各种PCI设备、ISA设备等。
+外设虚拟化主要有如下几种方式:
+纯软件模拟(完全虚拟化)
+QEMU最早的方案,虚拟机内核不用做任何修改,每一次对设备的寄存器读写都会陷入到KVM,进而到QEMU,QEMU再对这些请求进行处理并模拟硬件行为。
+软件模拟会导致非常多的QEMU/KVM接入,效率低下。
聪明的越界处理【未考虑到】
-/* 判断:如果传入的信号量是无效信号量,V操作失败,返回-1 */ |
毕竟有效的信号量都是引用的信号量表的信号量。所以地址越界的自然无效。
+virtio设备(半虚拟化)
+virtio设备是一类特殊的设备,并没有对应的物理设备,所以需要虚拟机内部操作系统安装特殊的virtio驱动。
+相比软件模拟,virtio方案提高了虚拟设备的性能。
最坑的一点
-其实指导书提醒了
---下面描述的问题未必具有普遍意义,仅做为提醒,请实验者注意。
-include/string.h 实现了全套的 C 语言字符串操作,而且都是采用汇编 + inline 方式优化。
-但在使用中,某些情况下可能会遇到一些奇怪的问题。比如某人就遇到
-strcmp()
会破坏参数内容的问题。如果调试中遇到有些 “诡异” 的情况,可以试试不包含头文件,一般都能解决。不包含string.h
,就不会用 inline 方式调用这些函数,它们工作起来就趋于正常了。
但是具体表现跟它说的差距有点大()
-我是全部检查没问题了,然后上linux0.11真机运行。PID:number这样的信息全部打印出来了,没啥问题,但是打印完操作系统就会寄,大多数极端情况就直接重启了,小部分还会温和地提醒以下报错信息然后死循环
-kernel panic: trying to free up swapper memory space |
最后尝试着修改去掉string.h,才得到了正确的结果,泪目。
+设备直通
+将物理硬件设备直接挂到虚拟机上,虚拟机直接与物理设备交互,尽可能在I/O路径上减少QEMU/KVM的参与。
+设备直通经常搭配硬件虚拟化支持技术SRIOV(Single Root I/O Virtualization,单根输入/输出虚拟化)使用,SRIOV能够将单个的物理硬件高效地虚拟出多个虚拟硬件。
-+参考文章:
-操作系统实验08-proc文件系统的实现
操作系统通过写设备的I/O端口或者MMIO地址来与设备交互,设备通过发送中断来通知操作系统事件。
+QEMU/KVM一方面需要完成这项中断设备的模拟,另一方面需要模拟中断的请求处理。
--在 Linux 0.11 上实现 procfs(proc 文件系统)内的 psinfo 结点。当读取此结点的内容时,可得到系统当前所有进程的状态信息。例如,用 cat 命令显示
-/proc/psinfo
和/proc/hdinfo
的内容,可得到:- -
$ cat /proc/psinfo
pid state father counter start_time
0 1 -1 0 0
1 1 0 28 1
4 1 1 1 73
3 1 1 27 63
6 0 4 12 817
$ cat /proc/hdinfo
total_blocks: 62000;
free_blocks: 39037;
used_blocks: 22963;
...-
procfs
及其结点要在内核启动时自动创建。相关功能实现在
-fs/proc.c
文件内。
--正式的 Linux 内核实现了
-procfs
,它是一个**虚拟文件系统**,通常被 mount(挂载) 到/proc
目录上,通过虚拟文件和虚拟目录的方式提供访问系统参数的机会,所以有人称它为 “了解系统信息的一个窗口”。这些虚拟的文件和目录**并没有真实地存在在磁盘**上,而是内核中各种数据的一种直观表示。虽然是虚拟的,但它们都可以通过标准的系统调用(
-open()
、read()
等)访问。其实,Linux 的很多系统命令就是通过读取
-/proc
实现的。例如uname -a
的部分信息就来自/proc/version
,而uptime
的部分信息来自/proc/uptime
和/proc/loadavg
。
--Linux 是通过文件系统接口实现
-procfs
,并在启动时自动将其 mount 到/proc
目录上。此目录下的所有内容都是随着系统的运行自动建立、删除和更新的,而且它们完全存在于内存中,不占用任何外存空间。
-Linux 0.11 还没有实现虚拟文件系统,也就是,还没有提供增加新文件系统支持的接口。所以本实验只能在现有文件系统的基础上,通过打补丁的方式模拟一个
-procfs
。Linux 0.11 使用的是 Minix 的文件系统,这是一个典型的基于
+inode
的文件系统,《注释》一书对它有详细描述。它的每个文件都要对应至少一个 inode,而 inode 中记录着文件的各种属性,包括文件类型。文件类型有普通文件、目录、字符设备文件和块设备文件等。在内核中,每种类型的文件都有不同的处理函数与之对应。我们可以增加一种新的文件类型——proc 文件,并在相应的处理函数内实现 procfs 要实现的功能。QEMU支持单CPU的Intel 8259中断控制器以及SMP的I/O APIC(I/O Advanced Programmable Interrupt Controller)和LAPIC(Local Advanced Programmable Interrupt Controller)中断控制器。在这种方式下,虚拟外设通过QEMU向虚拟机注入中断,需要先陷入到KVM,然后由KVM向虚拟机注入中断,这是一个非常费时的操作。
+为了提高虚拟机的效率,KVM自己也实现了中断控制器Intel 8259、I/O APIC以及LAPIC。用户可以有选择地让QEMU或者KVM模拟全部中断控制器,也可以让QEMU模拟Intel 8259中断控制器和I/O APIC,让KVM模拟LAPIC。
include/sys/stat.h 新增:
-介绍完上述的qemu虚拟化,接下来就可以对xv6的全启动进行一个梳理了。
+首先,在宿主机执行make qemu
。
在Makefile
中可以看到:
qemu: $K/kernel fs.img |
--psinfo 结点要通过
-mknod()
系统调用建立,所以要让它支持新的文件类型。
直接修改 fs/namei.c
文件中的 sys_mknod()
函数中的一行代码,如下:
if (S_ISBLK(mode) || S_ISCHR(mode) || S_ISPROC(mode)) |
在log中可以看到:
+... |
具体的Makefile
相关内容我不大了解,但结合输出,我想大概是先通过riscv64-linux-gnu-gcc
编译链接完所有文件,然后再执行mkfs
产生fs.img
镜像(mkfs
后面那些东西应该是文件参数,对应于源码中的读取可执行程序进磁盘的部分),最后再运行qemu-system-riscv64
开始对虚拟机进行boot。
boot直至启动后的所有代码,都是通过QEMU-KVM架构处理,直接运行在宿主机的CPU上的。其余的各种管理,可以详见小标题虚拟机在QEMU-KVM架构的执行方法
。
上面的知识表明,操作系统的启动在于文件系统初始化之后,这是因为操作系统本身的启动代码,放在磁盘映像fs.img
中,而fs.img
正是由文件系统初始化时弄出来的。也就是说,文件系统是操作系统的爸爸。【我以前一直以为是反过来的】
-+内核初始化的全部工作是在
-main()
中完成,而main()
在最后从内核态切换到用户态,并调用init()
。-
init()
做的第一件事情就是挂载根文件系统:+
void init(void) {
// ……
setup((void *) &drive_info);
// ……
}图中的boot块就是操作系统的引导扇区。
+
而mkfs
的作用,正是把宿主机提供的虚拟地址空间作为虚拟磁盘,把虚拟地址空间划分为如上图所示的地址结构。它是运行在宿主机当中的。有了mkfs
,才能有我们的虚拟机。
yysy这个就写得很好了。
+++linux的堆管理
+那么malloc到底是怎么实现的呢?不是每次要申请内存就调一下系统调用,而是程序向操作系统申请⼀块适当⼤⼩的堆空间,然后由程序⾃⼰管理这块空间,⽽具体来讲,管理着堆空间分配的往往是程序的运⾏库。
+也就是说,malloc本质上是以运行库而非系统调用形式出现的。它里面用到的是sbrk和mmap这两个系统调用来进货。
+glibc的malloc函数是这样处理⽤户的空间请求的:对于⼩于128KB的请求来说,它会在现有的堆空间⾥⾯,按照堆分配算法为它分配⼀块空间并返回;对于⼤于128KB的请求来说,它会使⽤mmap()函数为它分配⼀块匿名空间,然后在这个匿名空间中为⽤户分配空间。
+
在内核态中,我们使用kalloc
和kfree
来申请和释放内存页。在用户态中,我们使用malloc
和free
来对动态内存进行管理。【也就是说这个实现的是堆管理】
内核中的最小单位只能是页,但user mem-allocator对外提供的申请内存服务的最小单位不是页,而是sizeof(Header)
。因而,这就需要我们的user mem-allocator进行数据结构的管理,来统一这二者的实现。
user mem-allocator的数据结构是环形链表,起始结点为一个空数据载体。
+链表的头结点的存储地址/所代表的内存地址的地址数值最小,并且其余结点按遍历顺序地址递增。
+user mem-allocator由三个主要函数组成,分别是morecore
、malloc
和free
。一个一个地来说未免有点不符合正常人的思路,所以我接下来会以用户初次调用malloc
为例,来整理user mem-allocator的具体实现。
当用户初次调用malloc
,此时freep仍为空指针,因而会进入如下分支:
if((prevp = freep) == 0){ |
procfs
的初始化工作**应该在根文件系统挂载之后开始**。它包括两个步骤:
(1)建立 /proc
目录;建立 /proc
目录下的各个结点。本实验只建立 /proc/psinfo
。
(2)建立目录和结点分别需要调用 mkdir()
和 mknod()
系统调用。因为初始化时已经在用户态,所以不能直接调用 sys_mkdir()
和 sys_mknod()
。必须在初始化代码所在文件中实现这两个系统调用的用户态接口。
|
也即初始化为这种情况:
+随后,由于prevp->ptr == freep
,故而会在循环中进入该分支:
for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr){ |
mkdir()
时 mode 参数的值可以是 “0755”(对应 rwxr-xr-x
),表示只允许 root 用户改写此目录,其它人只能进入和读取此目录。
procfs 是一个只读文件系统,所以用 mknod()
建立 psinfo 结点时,必须通过 mode 参数将其设为只读。建议使用 S_IFPROC|0444
做为 mode 值,表示这是一个 proc 文件,权限为 0444(r–r–r–),对所有用户只读。
mknod()
的第三个参数 dev 用来说明结点所代表的设备编号。对于 procfs 来说,此编号可以完全自定义。proc 文件的处理函数将通过这个编号决定对应文件包含的信息是什么。例如,可以把 0 对应 psinfo,1 对应 meminfo,2 对应 cpuinfo。
也就是说,打开linux-0.11/init/main.c
-加入:
-
|
调用morecore
。
进入morecore
后,首先会对堆内存进行扩容:
if(nu < 4096) |
在init函数中,添加:
-mkdir("/proc",0755); |
其中,nu表示要申请的内存单元数,一个内存单元为sizeof(Header)
,因而nu在malloc
中计算如下:
nunits = (nbytes + sizeof(Header) - 1)/sizeof(Header) + 1; |
编译运行即可看到:
-为了满足内核以一页为最小内存单位的需求,以及避免过多陷入内核态,它每次会申请至少4096*内存单元的堆空间。
+对堆内存进行扩容完之后,morecore
会手动调用一次free
,将新申请到的内存加入数据结构中。【此处类似于在knit
中调用kfree
的原理】
void free(void *ap){ |
由于此时freep == freep->str == base
,并且我们在morecore
中新申请的内存空间ap
满足ap > base
,故而会跳出循环。
--这些信息至少说明,psinfo 被正确
-open()
了。所以我们不需要对sys_open()
动任何手脚,唯一要打补丁的,是sys_read()
。
--首先分析
-sys_read
(在文件fs/read_write.c
中)要在这里一群 if 的排比中,加上
-S_IFPROC()
的分支,进入对 proc 文件的处理函数。需要传给处理函数的参数包括:-
+- -
inode->i_zone[0]
,这就是mknod()
时指定的dev
——设备编号- -
buf
,指向用户空间,就是read()
的第二个参数,用来接收数据- -
count
,就是read()
的第三个参数,说明buf
指向的缓冲区大小- -
&file->f_pos
,f_pos
是上一次读文件结束时“文件位置指针”的指向。这里必须传指针,因为处理函数需要根据传给buf
的数据量修改f_pos
的值。为什么
+ap > base
呢?别忘了我们扩容的原理。我们是以
proc->size
为起始地址扩容的。ap处在扩容内存中,因而ap>旧size;base处在扩容前内存内,因而base<=旧size。故而有ap>base。
依照指导书,在read_write.c添加如下语句:
-extern int proc_handler(unsigned short dev,char* buf,int count,off_t* f_pos); |
if(bp + bp->s.size == p->s.ptr){ |
--proc 文件的处理函数的功能是根据设备编号,把不同的内容写入到用户空间的 buf。写入的数据要从
-f_pos
指向的位置开始,每次最多写 count 个字节,并根据实际写入的字节数调整f_pos
的值,最后返回实际写入的字节数。当设备编号表明要读的是 psinfo 的内容时,就要按照 psinfo 的形式组织数据。实现此函数可能要用到如下几个函数:
--
-- malloc() 函数
-- free() 函数
-包含
-linux/kernel.h
头文件后,就可以使用malloc()
和free()
函数。它们是可以被核心态代码调用的,唯一的限制是一次申请的内存大小不能超过一个页面。
-+进程的信息就来源于内核全局结构数组
-struct task_struct * task[NR_TASKS]
中,具体读取细节可参照sched.c
中的函数schedule()
。可以借鉴一下代码:
--
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1)+...;
跳出循环后,我们会进入第一个if的第二个分支,以及第二个if的第二个分支。经过这些指针操作后,此时我们的数据结构如下图所示:
+也即形成了一个两节点的环形链表。
+经历完上述调用后,我们回到malloc
的循环中:
for(p = prevp->s.ptr; ; prevp = p, p = p->s.ptr){ |
由morecore
的返回值可知,此时我们的p应该指向freep。本轮循环结束后执行 p = p->s.ptr
,此时我们的p指向了我们刚在morecore
中扩容出来的那一大段内存。
在下一轮循环中,由于我们刚刚通过morecore
申请了至少nunits
的空间,因而我们将进入该分支:
if(p->s.size >= nunits){ |
当nunits >= 4096
,也即p->s.size == nunits
,p所指向的地址恰好就是我们接下来会用的地址。因而,我们就将这部分内存空间从我们的freelist中剔除,在之后返回p的地址即可。
当nunits < 4096
,也即p->s.size != nunits
,说明p所指向的这块内存空间比我们需要的大,那么我们就仅将该段内存空间切割出需要的那一小部分,再把p指向那一小部分开头的地方,返回p地址即可,如图所示。
这样一来,我们就成功给用户它所需要的内存空间了。
+进行malloc之后,用户还需要调用free来手动释放内存,防止内存泄漏。
+for(p = freep; !(bp > p && bp < p->s.ptr); p = p->s.ptr) |
由于ap > base
且ap > 旧p->size = base->ptr
且base < base->ptr
,故而首先会进行一轮循环。再然后,由于p = 旧p->size
,并且p > p->ptr = base
,并且ap > 旧size
,故而跳出循环。
--cat 是 Linux 下的一个常用命令,功能是将文件的内容打印到标准输出。
-它核心实现大体如下:
-+
int main(int argc, char* argv[])
{
char buf[513] = {'\0'};
int nread;
int fd = open(argv[1], O_RDONLY, 0);
while(nread = read(fd, buf, 512))
{
buf[nread] = '\0';
puts(buf);
}
return 0;
}此处循环中,循环语句内部的这个循环实际上是对遍历到环形链表尾部,即将从头开始遍历,这个边界情况的处理。比较符合逻辑的还是循环语句内的那个条件。
在cat的代码中,open函数返回了psinfo的文件描述符,read函数读到该文件描述符,就会识别出我们要读写的文件是PROC类型的,因此就会跳转到我们的proc_handler去执行,再进一步跳转到psinfo_handler执行。根据cat的代码和指导书的提示,不难得出,我们的目标就是把进程的信息按照格式给弄进buf里面,就可以了。
-而这也正体现了proc作为“**虚拟文件**”的特点。对它进行读写,它的信息并非存放在磁盘中,而是全部由放在内存中的逻辑和数据【由task_struct提供】来完成。
-在fs文件夹下创建文件proc_dev.c,编写proc文件的处理函数。代码如下:
-
|
if(bp + bp->s.size == p->s.ptr){ |
运行结果:
-此时会进入第二个if的第一个分支。具体情况看图就行,不多bb。
+主要就是这个数据结构用得很巧妙但也很复杂。它吸取了内核态中分配内存使用一个freelist的特点,同时又巧妙地利用了内存地址有序的特点,从而实现碎片内存管理。我的建议是多画图。
+还有其实有一点我不是很理解。我觉得freep
这个变量的用意非常不明,它似乎并不是指代整个freelist的头,因为它在很多个地方都诡异地赋值了一次。我想,它也许始终指向上一次被alloc/被free的内存的前一个吧。。。我猜测这样设计是为了蕴含一些LRU的思想。不大明白。
由os知识可知,机器态、内核态、用户态分别有三种不同的操作权限。xv6是如何对权限切换进行管理的呢?
+这部分知识我在正文的一个小地方记录了下来,详见 chapter2 - Code: starting xv6 and the fifirst process - xv6 - 感想 的第二点。
+在xv6该次实验中,为了实现评测可视化,引入了statistics机制对结果进行评估。下面,我将通过源码简单介绍其实现机制。
+来讲讲这玩意是怎么实现用户态读取锁争用次数的。我们从statistics
函数可看出,它的本质是通过读取“文件”,来从内核中读取争用次数的相关数据:
int statistics(void *buf, int sz) { |
1.LAST_TASK 的定义
-对于LAST_TASK,我本来的理解是,当前所有进程的最后一个。
-本来我设的是跟schedule一样,另p=LAST_TASK,从末尾开始打印。我那时其余代码跟上面一样,就只是把上面的FIRST改成LAST,结果输出为空,调试发现LAST_TASK==NULL。
-然后打开sched.h,看到LAST_TASK的定义:
-那么安装以前所学的内容,我们很容易联系到这玩意应该并不是个文件,而是类似于proc文件系统那样的虚拟文件。它应该会在open、read中根据其特有的文件类型进行转发。在init.c
中,我们可以看到:
main(void) |
原来它就是单纯简单粗暴地指“最后一个”进程23333
-我们目前当前的进程数量远远小于进程的最大数量,因此最大数量编号的那个进程自然也就是空的了。
-2.char s[100]={0};
-用这个的时候编译报错:undefined reference to ’memset‘
-说明这个简略写法其实本质是用的memset,而要用memset的话需要包含头文件string.h。经测试得包含了string.h后确实就好使了。
-//s_imap_blocks、ns_zmap_blocks、 |
这玩意的文件结点实际上是在创建console时整的,并且其有一个特殊的文件类型“STATS”。我们可以进一步追溯到kernel中的main.c
:
void main() |
3.我发现一件事
-我第一次把init/main.c写错了,写成:
-mkdir("/proc",0755); |
void |
设别号忘了改了。然后进行了一次编译,运行。
-之后我发现错了,就改成了
-mkdir("/proc",0755); |
可以看到,它给这个STATS
文件类型注册了这两个函数。当我们调用read和write时,实际上就是在调用这俩玩意。我们可以看下这两个handler都干了啥。
|
再次编译运行,结果上面的那个错还是没改回来
-直到我手动把proc文件夹删了,再重新读一次磁盘加载proc文件夹,才回归正常。
-本次实验耗时:下午一点到晚上九点半()
-本实验通过对proc虚拟文件的编写流程,实际上让我们体会到了“一切皆文件”的思想。
-什么东西都可以是文件,只不过它们有不同的文件类型和不同的read/write处理函数。
-对于终端设备和磁盘,其read/write函数本质上是在用out指令跟它的缓冲区交互,只不过磁盘比终端设备抽象层次更深,包含了文件系统的层层封装。
-对于虚拟文件,其read/write函数本质上就是与内存交互,通过一段逻辑【处理函数】将内存存储的当前操作系统信息实时显示出来,而不需要存储。
-还有,参考文章那篇的代码写的很好,快去看!
+int statswrite(int user_src, uint64 src, int n) { // WARNING: READ ONLY!!! |
可以看到其本质就是把statslock
返回的东西copy到用户空间了。我们来结合最后的输出效果看看statslock
的具体实现:
int statslock(char *buf, int sz) { |
可以看到其争用本质计算是通过spinlock::nts
字段记录。我们来看看这玩意的引用:
void initlock(struct spinlock *lk, char *name) { |
很好,逻辑很简单,就是记录acquire时等待的次数,非常简单粗暴(((
+总的来说这个思路还是挺酷的,而且这个“一切皆文件”的思想再次震撼了我,一个小小的xv6确实能做到那么多。
+]]>防火墙
+sudo ufw status numbered # 查看 |
http://localhost/webdemo4_war/*.do
。
iSCSI将SCSI命令和数据(SCSI 协议)封装为IP包,通过TCP/IP传输。
主机连接
-有标准NIC、TOE NIC、iSCSI HBA三种方案。
-标准NIC就是标准网卡只提供IP层,TCP、iSCSI还需要os来处理;TOE NIC只卸载了TCP/IP 协议层,iSCSI还需要os来处理;iSCSI HBA一下全卸载了 iSCSI 协议层和TCP/IP 协议层,主机p都不用管。
-拓扑结构
-iSCSI的拓扑结构可分为两类:原生模式和桥接模式。
-原生模式没有FC(光纤通信)组件。iSCSI 发起方可以直接连接到到目的方或通过 IP 网络连接到目的方;桥接模式通过提供iSCSI到FC的桥接功能以实现FC与IP共存。例如,iSCSI发起方可以在IP环境中,而存储设备仍然留在FC SAN 环境中。
-FCIP是一种隧道协议,使分散的FC SAN 孤岛通过现有的IP 网络进行互联。相当于就是在原本的FC SAN(光纤网络)之间加了个IP协议,把孤岛FC SAN联结起来。
-就相当于是把FC交换机和IP交换机组合起来了,减少服务器的端口。聚合网络适配器(CNA) 是一个结合了标准NIC 和FC HBA 两者功能的适配器,实现了两种流量的合并。有了CNA就不必配置两种适配器,它和线缆分别用于FC和以太网通信,减少了所需的服务器插槽数和交换机端口数。FCoE交换机同时具有以太网交换机和FC交换机的功能。
-网络链接存储(network-attached storage,NAS)
-SAN是以块为单位进行收发数据的,NAS是以文件为单位。它允许用户通过局域网或互联网连接到存储设备并访问和管理其中的数据。NAS 协议有多种实现方式,包括 NFS、CIFS/SMB、FTP、HTTP 和 iSCSI 等。它是一种基于文件级别的存储方案,可以提供高效的数据共享和备份,并支持多用户同时访问。
-涉及文件IO和块级IO的转化:
-网络文件共享的实例:
-统一NAS提供文件服务,同时负责存储文件数据,并提供块级数据访问。它支持用于文件访问的CIFS和NFS协议,以及用于块级访问的SCSI和FC协议。
-基于对象的存储(object-based storage devices,OSD)
-OSD以对象的形式存储数据。它使用扁平地址空间(flat address space)来存储数据。这种地址空间中没有目录和文件的分层,所以我们访问内容就会采用按内容寻址(CAS)的方法。CAS通常使用哈希值来唯一标识数据块,并通过维护哈希值与存储块物理地址之间的映射关系来实现查找。
-一个节点就是一台运行OSD操作系统、提供数据存储、获取和管理服务的服务器,负责维护对象ID与文件系统名称空间的映射。
-备份有三个目的:灾难恢复、业务性恢复和归档。
-三种形式:全备份、增量备份、累计备份。增量备份是相对于上一次的增量,累积备份是针对于上一次全备份的增量。
-也没什么好说的,就记得客户端就是发备份数据的,服务器端有点类似CPU,指挥客户端发数据、存储结点接数据,以及存储结点把数据存入备份设备。
-备份环境中采用三种基础的拓扑结构:直接连接备份、基于局域网备份、基于SAN备份。
-相当于本来app和备份设备直连,现在改成网络连接,优化备份设备利用率:
-然而上面方法数据流太大,网络性能不行。所以我们只对元数据采用LAN,而对备份数据流采用更快的FC SAN:
-数据去重(data deduplication,又称重复数据删除)是识别重复数据并将其删除的过程。在备份时如果检测到重复数据,就会将其丢弃,然后创建指针指向已备份过的数据副本。
-去重有两种方法:文件级或子文件级。
-子文件去重将文件划分为更小的块,有两种实现形式:固定长度块和长度可变段。固定长度块去重将文件划分为固定长度的块,使用哈希算法找出重复的数据。
-固定长度块虽然简单,但是可能会错过不少重复数据,因为相似数据的块边界可能不同;在长度可变段去重中,如果一个段中有变化,那么只有此段的边界被调整,剩余段不变。与固定块方法相比,这一方法大幅提升了识别重复数据段的能力。
-这段话详细介绍了副本与备份的区别:
-本章大概讲的就是本地复制,将产生副本的具体几种技术,如LVM镜像和文件系统镜像(基于本地主机)、全卷镜像指针全复制指针虚拟复制(基于存储阵列)、CDP连续数据保护(基于网络)。它们的共通思想就是使用COW/COA(access)、虚拟指针和全复制的差异,以及日志/位图。
-有同步和异步之分,同步就是同时写源和目标,全部写完再响应;异步就是先写源,然后响应,然后再写目标。
-具体的方法依然是有三个角度,LVM逻辑卷同步异步写和仅传输日志(基于主机)、基于磁盘缓存的远程复制(基于存储阵列)、远程CDP(相当于通过SAN连接本地CDP和远程CDP,基于网络)
-然后更常见的是使用三站点法。
-直线型
-源 (同步复制) 中间站点(异步复制/磁盘缓冲)远程站点
-三角型
-看到题目时,我首先联想到的是mealy型状态机,因为我联想到了序列检测。课内的序列检测讲的时候是把它当做mealy型的。但看了标准作答之后,才发现它其实应该是moore型。这让我对这二者的区别产生了深深的不解。
-原来对于moore型状态机和mealy型状态机的理解仅仅停留在概念上,“moore型状态机的输出与输入无关,只与当前状态有关”“mealy型状态机输出与输入和现态都有关”。但这其实是一句非常抽象的话:什么是“无关”,什么是“有关”?moore型状态机的状态不也是依据输入进行转移的吗?那么这算不算“有关”?
-探究之后,我得到了更精确的“有关”“无关”的定义。
-- --- -
-
--来自:Moore状态机和Mealy状态机的区别(以序列检测器为例)
-Moore状态机输出只与此时的状态有关,因此假如需要检测宽度为4的序列,则需要五个状态。
-Mealy状态机输出与此时的状态以及输入有关,因此假如需要检测宽度为4的序列,只需要四个状态即可。
-
联想到我们课上学习的序列检测:
-它这明明长度为3的序列用了4个状态,应该算是moore型,为什么我们却被教说序列检测器是mealy型状态机呢?
-原因是因为,我们进行了状态化简这一步,将moore型状态机转化为了mealy型状态机。
---这俩是可以相互转化的
- -把Moore机转换为Mealy机的办法为,把次态的输出修改为对应现态的输出,同时合并一些具有等价性能的状态。把Mealy机转换为Moore机的办法是,把当前态的输出修改为对应次态的输出,同时添加一些状态。如图1所示,为把Mealy机状态图转化为Moore机状态图。
--
图1 Mealy型机转换为Moore型机如图1所示,把Mealy型机转换为Moore型机,只要把现时输出改变为下一时刻输出。对于状态A,有4个箭头指向它,表示在当前状态下有4个状态可以转换为下一状态的A;同时当前输出均为0,可以把0移入状态A内部,表示在Moore机中状态A的输出为0。同理,可以把0分别移位B/C状态。但对于状态D,有两个箭头指向且具有不同的输出值,需要把状态D分解成两个状态D1和D2(每个状态对应一个输出,当输出不同需要利用不同的状态表示,这即是Moore机具有更多状态的原因),得到完整的Moore机状态模型。
-同理,若把上图的Moore机转换为Mealy机,只要把Moore机中下一状态的输出改变成Mealy机中当前状态的输出,由于D1/D2两状态处于A/C两状态之间,且相当于A/C节点之间的一个等效节点,可以把D1/D2两状态合并为一个状态。
-
- --并非所有时序电路都可以使用Mealy模型实现。 一些时序电路只能作为摩尔机器实现。
-
所以,我们可以出此暴论:在课程范围内,首先以moore的思想来设计状态机。如果该状态机可以被化简,那么这道题就要用mealy型的来做;如果不能,那么这道题就是得用moore型状态机来做。
-一开始的那个时序锁的moore状态机不能化简,因此它是moore型。
---]]>这个点本来可以讲得更清楚一些的……只教会我们做题的套路有啥意思呢←_←
-
在传统的BIOS系统中,计算机具体的启动流程如下:
-在本次内核编译配置过程中,最主要探究的是文件系统的装载过程,也即介于6-7之间的部分。
-文件系统在启动流程中的发展历程可以分为以下三个部分:
-GRUB文件系统
-由 GRUB 自身通过 BIOS 提供的服务加载
-initramfs
-由GRUB加载,用于挂载真正的文件系统
-真正的根文件系统
-下面,将介绍1和2两个流程。
---GRUB(GNU GRand Unified Bootloader)是一种常用的引导加载程序,用于在计算机启动时加载操作系统。
-GRUB的主要功能是在计算机启动时提供一个菜单,让用户选择要启动的操作系统或内核。它支持多个操作系统,包括各种版本的Linux、Windows、BSD等。通过GRUB,用户可以在多个操作系统之间轻松切换。
-除了操作系统选择,GRUB还提供了一些高级功能,例如引导参数的设置、内存检测、系统恢复等。它还支持在启动过程中加载内核模块和初始化RAM磁盘映像(initrd或initramfs)。
-GRUB具有高度可配置性,允许用户自定义引导菜单、设置默认启动项、编辑内核参数等。它还支持引导加载程序间的链式引导,可以引导其他引导加载程序,如Windows的NTLDR。
-
GRUB的基本作用流程为:
-grub.cfg
配置文件内核启动时,GRUB程序会读取/boot/grub/
目录下的GRUB配置文件grub.cfg
,其中记录了所有GRUB菜单可供选择的内核选项(menuentry)及其对应的启动依赖参数。以6.4.0内核选项为例:
menuentry标识着GRUB菜单中的一个内核选项 |
可以看到,grub.cfg
主要记录了一些该内核启动需要的依赖module,以及内核映像和initramfs映像的路径。
menuentry的代码中,有以下几个要点值得注意:
-insmod gzio
由于加载gzio模块,提供对GZIP压缩和解压缩功能的支持。
-看到这里我第一反应是觉得有点割裂,为啥这看着比较无关紧要的解压缩功能要在内核启动之前就需要有呢?于是我想起来在配置内核时,有一个选项是这样的:
-在配置选项中,我们选择了对initramfs的支持,并且勾选了Support initial ramdisk/ramfs compressed using gzip
,也即在编译时通过gzip压缩initramfs的大小以节省空间。
所以说,我们在内核启动之前,持有的initramfs处于被压缩的状态。故而,我们自然需要在内核启动之前安装gzio模块,从而支持之后对initramfs的解压缩了。
-insmod ext2
这句代码说明,GRUB的临时文件系统为ext2类型,这句代码事实上是在安装GRUB建立临时文件的必要依赖包,从而GRUB程序之后才能建立其临时文件系统、从/boot/initrd.img获取initramfs映像。
-linux /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro text
指定了启动参数,也即将根文件系统以只读(ro
)的方式挂载在root=UUID=XXX
对应的块设备上,并且默认以text
方式(也即非图形化的Shell界面)启动内核。
此处的启动参数可在下一个部分介绍的grub
文件中个性化。
实际运用中,很多时候需要对启动参数进行一些修改。下面介绍两种修改grub.cfg
的方法。
可以看到,grub.cfg
其实格式较为固定(也即由一系列内容也比较相似的menuentry构成)。因而,实际上我们是通过grub.d
生成grub.cfg
的(6.S081实验中事实上也涉及了这一点),而/etc/default/grub
则是GRUB程序以及grub.cfg
生成的配置文件。下面介绍下该文件主要有哪些配置选项。
If you change this file, run 'update-grub' afterwards to update |
重点看下这几个参数:
-GRUB_CMDLINE_LINUX
表示最终生成的grub.cfg中的每一个menuentry中的linux那一行需要附加什么参数。
-例如说,如果设置为:
-表示initramfs在挂载真正的根文件系统之前,需要等待120s,用于防止磁盘没准备好导致的挂载失败 |
那么,最终在menuentry中的启动参数就为:
-linux /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro rootdelay=120 text |
其他一些常见的选项:
-直接以路径来标识块设备而非使用UUID。此为old option,建议尽量使用UUID |
GRUB_DEFAULT
参考 可以用来指定重启时的内核选项。如GRUB_DEFAULT="1> 0"
表示选择第一个菜单界面的第2栏(Advanced for Ubuntu)和第二个菜单的第1个内核。
在修改完grub
文件之后,我们需要执行sudo update-grub
,来重新生成grub.cfg
文件供下次启动使用。
我们可以在GRUB界面选中所需内核,按下e键:
-然后就可以对启动参数进行修改,^X退出。
-值得注意的是,此修改仅对本次启动有效。如果需要长期修改,建议还是通过第一种方法去修改。
-GRUB程序会通过initrd.img
启动initramfs,从而进行真正的根文件系统挂载。
--initrd.img是一个Linux系统中的初始化内存盘(initial RAM disk)的映像文件。它是一个压缩的文件系统映像,通常在引导过程中加载到内存中,并提供了一种临时的根文件系统,以便在正式的根文件系统(通常位于硬盘上)可用之前提供必要的功能和模块。
-
我们可以通过unmkinitramfs /boot/initrd.img-6.4.0-rc3+ /tmp/initrd/
命令解压initrd,探究里面到底有什么玩意。
├── bin -> usr/bin |
可以看到,这实际上就是一个小型的文件系统,也即initramfs。它有自己的built-in Shell(BusyBox):
-有一些较少的Shell命令(bin和sbin目录下),以及用来挂载真正的根文件系统的代码逻辑(存储在scripts目录下)。【我猜】在正常情况下,系统会执行scripts下的脚本代码挂载真正的文件系统。当挂载出现异常时,系统就会将控制权交给initramfs内置的Shell BusyBox,由用户自己探究出了什么问题。
-我们接下来可以追踪下initramfs的script目录下的文件系统挂载流程。
-挂载真正文件系统的主要函数为local_mount_root
:
仅展示主要流程代码 |
由于研究这个是错误驱动(乐),因而我只主要看了下local_device_setup
:
$1=device ID to mount设备ID |
可以看到,这里如果进入错误状态,最终就是这样的效果2333:
-此为《信息存储与管理(第二版):数字信息的存储、管理和保护》的看书总结,相当于是对存储技术的一个简单的名词入门。
- -本章节我印象最深的还是以前就不大了解的DNS,今天看到书的描写真有种豁然开朗的感觉。
-DNS服务器用于保存域名—IP地址的映射对,为了增加查找效率,DNS根据域名的分级采用树形组织,例如hitsz.edu.cn/
可以相当于是/cn/edu/hitsz
,包含了/
、cn
、edu
这几个域。根DNS服务器存储着根域,记录了所有一级域名对应DNS服务器的IP地址。所有的DNS服务器都会保存根服务器的IP地址。
--世界上只有13个根DNS服务器IP地址,但是有很多台根DNS服务器。
-
主机需要手动配置DNS服务器地址。
-当浏览器需要填写请求头时,它需要通过系统调用向操作系统发送DNS查询请求。操作系统将DNS请求发送给配置在主机上的DNS服务器(下称A),A再向根DNS服务器发送请求。根DNS服务器解析域名,返回下一级DNS服务器的IP地址。A再向下级DNS服务器再次发送请求,下级再返回下下级IP地址。以此类推,最终A就能得到目标IP地址的正确响应。整个过程如下图所示:
-与此同时,各个DNS服务器都会有定时刷新的缓存,从而加速了查找效率。
-本章前面大多讨论TCP/IP具体协议内容,以前已经了解过很多次了就不多赘述。所以TCP/IP部分就以分点的形式随意列举一下:
-IP 中还包括 ICMPA 协议和 ARPB 协议。ICMP 用于告知网络包传送过程中产生的错误以及各种控制消息,ARP 用于根据 IP 地址查询相应的以太网 MAC 地址。
-套接字中记录了用于控制通信操作的各种控制信息,协议栈则需要根据这些信息判断下一步的行动,【包括应用程序信息和协议栈状态信息】这就是套接字的作用。所以需要针对不同协议栈实现不同的socket。
+有标准NIC、TOE NIC、iSCSI HBA三种方案。
+标准NIC就是标准网卡只提供IP层,TCP、iSCSI还需要os来处理;TOE NIC只卸载了TCP/IP 协议层,iSCSI还需要os来处理;iSCSI HBA一下全卸载了 iSCSI 协议层和TCP/IP 协议层,主机p都不用管。
是的,回想当初CS144,也是socket来负责有特定消息时调用TCP相关函数来通知处理。
+拓扑结构
+iSCSI的拓扑结构可分为两类:原生模式和桥接模式。
+原生模式没有FC(光纤通信)组件。iSCSI 发起方可以直接连接到到目的方或通过 IP 网络连接到目的方;桥接模式通过提供iSCSI到FC的桥接功能以实现FC与IP共存。例如,iSCSI发起方可以在IP环境中,而存储设备仍然留在FC SAN 环境中。
+连接 connect
-连接实际上是通信双方交换控制信息,在套接字中记录这些必要信息并准备数据收发的一连串操作。
+FCIP是一种隧道协议,使分散的FC SAN 孤岛通过现有的IP 网络进行互联。相当于就是在原本的FC SAN(光纤网络)之间加了个IP协议,把孤岛FC SAN联结起来。
+就相当于是把FC交换机和IP交换机组合起来了,减少服务器的端口。聚合网络适配器(CNA) 是一个结合了标准NIC 和FC HBA 两者功能的适配器,实现了两种流量的合并。有了CNA就不必配置两种适配器,它和线缆分别用于FC和以太网通信,减少了所需的服务器插槽数和交换机端口数。FCoE交换机同时具有以太网交换机和FC交换机的功能。
+网络链接存储(network-attached storage,NAS)
+SAN是以块为单位进行收发数据的,NAS是以文件为单位。它允许用户通过局域网或互联网连接到存储设备并访问和管理其中的数据。NAS 协议有多种实现方式,包括 NFS、CIFS/SMB、FTP、HTTP 和 iSCSI 等。它是一种基于文件级别的存储方案,可以提供高效的数据共享和备份,并支持多用户同时访问。
+涉及文件IO和块级IO的转化:
+网络文件共享的实例:
动态调整等待时间
-统一NAS提供文件服务,同时负责存储文件数据,并提供块级数据访问。它支持用于文件访问的CIFS和NFS协议,以及用于块级访问的SCSI和FC协议。
+基于对象的存储(object-based storage devices,OSD)
+OSD以对象的形式存储数据。它使用扁平地址空间(flat address space)来存储数据。这种地址空间中没有目录和文件的分层,所以我们访问内容就会采用按内容寻址(CAS)的方法。CAS通常使用哈希值来唯一标识数据块,并通过维护哈希值与存储块物理地址之间的映射关系来实现查找。
+一个节点就是一台运行OSD操作系统、提供数据存储、获取和管理服务的服务器,负责维护对象ID与文件系统名称空间的映射。
+备份有三个目的:灾难恢复、业务性恢复和归档。
+三种形式:全备份、增量备份、累计备份。增量备份是相对于上一次的增量,累积备份是针对于上一次全备份的增量。
+也没什么好说的,就记得客户端就是发备份数据的,服务器端有点类似CPU,指挥客户端发数据、存储结点接数据,以及存储结点把数据存入备份设备。
+备份环境中采用三种基础的拓扑结构:直接连接备份、基于局域网备份、基于SAN备份。
+相当于本来app和备份设备直连,现在改成网络连接,优化备份设备利用率:
+然而上面方法数据流太大,网络性能不行。所以我们只对元数据采用LAN,而对备份数据流采用更快的FC SAN:
+数据去重(data deduplication,又称重复数据删除)是识别重复数据并将其删除的过程。在备份时如果检测到重复数据,就会将其丢弃,然后创建指针指向已备份过的数据副本。
+去重有两种方法:文件级或子文件级。
+子文件去重将文件划分为更小的块,有两种实现形式:固定长度块和长度可变段。固定长度块去重将文件划分为固定长度的块,使用哈希算法找出重复的数据。
+固定长度块虽然简单,但是可能会错过不少重复数据,因为相似数据的块边界可能不同;在长度可变段去重中,如果一个段中有变化,那么只有此段的边界被调整,剩余段不变。与固定块方法相比,这一方法大幅提升了识别重复数据段的能力。
+这段话详细介绍了副本与备份的区别:
+以太网的定义
-系统初始化时MAC地址的设置
-MAC地址是上电后由驱动程序从ROM中读取的,而非自动获取的
-电信号转换【这个帅得不行】
-为了区分连续的1或0,我们就需要同时发送数据信号和时钟信号,然而这样开销太大,因而我们引入了上升沿。
-上升沿本质上是数据信号和时钟信号叠加而成的结果,叠加方式是异或。
-提到异或是否感觉豁然开朗?是的,这东西恢复时也是使用了异或的性质:接收方从帧头获取时钟频率从而得到时钟信号,跟收到的叠加信号进行再次叠加(异或),就可以获得原来的数据信号了。
-我只能说牛逼,一直以来对异或的视角还停留在单纯的数字,这个波形的物理概念真的惊到我了。
-实例:
-半双工模式【同一时刻只能进行发or收】使用集线器,全双工模式【发or收可以并行】使用交换机。半双工模式需要进行载波监听碰撞检测。
+本章大概讲的就是本地复制,将产生副本的具体几种技术,如LVM镜像和文件系统镜像(基于本地主机)、全卷镜像指针全复制指针虚拟复制(基于存储阵列)、CDP连续数据保护(基于网络)。它们的共通思想就是使用COW/COA(access)、虚拟指针和全复制的差异,以及日志/位图。
+有同步和异步之分,同步就是同时写源和目标,全部写完再响应;异步就是先写源,然后响应,然后再写目标。
+具体的方法依然是有三个角度,LVM逻辑卷同步异步写和仅传输日志(基于主机)、基于磁盘缓存的远程复制(基于存储阵列)、远程CDP(相当于通过SAN连接本地CDP和远程CDP,基于网络)
+然后更常见的是使用三站点法。
+直线型
+源 (同步复制) 中间站点(异步复制/磁盘缓冲)远程站点
服务器的操作系统具备和路由器相同的包转发功能,当打开这一功能时,它就可以像路由器一样对包进行转发。在这种情况下,当收到不是发给自己的包的时候,就会像路由器一样执行包转发操作。
+三角型
+http://localhost/webdemo4_war/*.do
。
--来到指导书最高点!太美丽了xv6。哎呀那不文件系统吗(
-这里是自底向上讲起的。之后可以看看hit网课的自顶向下。
-
---
--The disk layer reads and writes blocks on an virtio hard drive.
-The buffer cache layer caches disk blocks and synchronizes access to them, making sure that only one kernel process at a time can modify the data stored in any particular block.
-The logging layer allows higher layers to wrap updates to several blocks in a transaction, and ensures that the blocks are updated atomically in the face of crashes (i.e., all of them are updated or none). 【日志记录层允许更高层将更新包装到一个事务中的多个块,并确保在崩溃时以原子方式更新块(即,所有块都更新或不更新)。可以类比一下数据库的那个概念。】
-The inode layer provides individual files, each represented as an inode with a unique i-number and some blocks holding the file’s data.
-The directory layer implements each directory as a special kind of inode whose content is a sequence of directory entries, each of which contains a file’s name and i-number.
-The pathname layer provides hierarchical path names like /usr/rtm/xv6/fs.c, and resolves them with recursive lookup.
-The file descriptor layer abstracts many Unix resources (e.g., pipes, devices, fifiles, etc.) using the file system interface, simplifying the lives of application programmers.
-
--The file system must have a plan for where it stores inodes and content blocks on the disk. To do so, xv6 divides the disk into several sections, as Figure 8.2 shows.
-The file system does not use block 0 (it holds the boot sector).
-Block 1 is called the superblock; it contains metadata about the file system (the file system size in blocks, the number of data blocks, the number of inodes, and the number of blocks in the log). The superblock is filled in by a separate program, called
-mkfs
, which builds an initial file system.Blocks starting at 2 hold the log.
-After the log are the inodes, with multiple inodes per block.
-After those come bitmap blocks tracking which data blocks are in use. 【应该是用来标识每个块是否空闲的吧】
-The remaining blocks are data blocks; each is either marked free in the bitmap block, or holds content for a file or directory【要么空闲要么是文件或目录】.
+哈工大操作系统实验 +/2022/10/04/%E5%93%88%E5%B7%A5%E5%A4%A7%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%AE%9E%E9%AA%8C/ ++ 实验入口
主要参考文章
lseek()函数:用于移动打开文件的指针
linux系统调用之write源码解析(基于linux0.11)
get_fs_bytes解析
VIM与系统剪贴板的复制粘贴
操作系统实验六 信号量的实现和应用(哈工大李治军)
哈工大操作系统实验6 信号量的实现 pc.c 编译时报错 对‘sem_open‘未定义的引用
Linux 文件编程 open函数
哈工大-操作系统-HitOSlab-李治军-实验5-信号量的实现和应用
-The buffer cache has two jobs:
--
-- synchronize access to disk blocks to ensure that only one copy of a block is in memory and that only one kernel thread at a time uses that copy;
-- cache popular blocks so that they don’t need to be re-read from the slow disk.
-The code is in
-bio.c
.Buffer cache中保存磁盘块的缓冲区数量固定,这意味着如果文件系统请求还未存放在缓存中的块,Buffer cache必须回收当前保存其他块内容的缓冲区。Buffer cache为新块回收最近使用最少的缓冲区。这样做的原因是认为最近使用最少的缓冲区是最不可能近期再次使用的缓冲区。
+地址映射与共享
+-参考文章
+ +-
数据结构定义
+
struct buf {
int valid; // has data been read from disk?缓冲区是否包含块的副本
int disk; // does disk "own" buf?缓冲区内容是否已交给磁盘
uint dev;
uint blockno;
struct sleeplock lock;
uint refcnt;
struct buf *prev; // LRU cache list
struct buf *next;
uchar data[BSIZE];
};必备知识
要点1 共享内存
顾名思义,共享内存就是允许两个不相关的进程访问同一个逻辑内存。共享内存是在两个正在运行的进程之间共享和传递数据的一种非常有效的方式。不同进程之间共享的内存通常安排为同一段物理内存。如果某个进程向共享内存写入数据,所做的改动将立即影响到可以访问同一段共享内存的任何其他进程。
+注:共享内存并未提供同步机制,所以我们需要用信号量来实现同步。
+Linux提供了一组接口用于使用共享内存,它们声明在头文件 sys/shm.h 中。
+1.shmget
程序先通过调用shmget()函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget()函数的返回值)。
+-
int shmget(key_t key, size_t size, int shmflg);这应该代表着一个磁盘块。
-+
struct {
struct spinlock lock;
struct buf buf[NBUF];
// Linked list of all buffers, through prev/next.
// Sorted by how recently the buffer was used.
// head.next is most recent, head.prev is least.
struct buf head;
} bcache;key为共享内存段名字,size为大小,shmflg是权限标志
+注:
+① key:非0整数,共享内存段的命名
+② shmflag:作用与open函数的mode参数一样,比如IPC_CREAT,或连接
+共享内存的权限标志与文件的读写权限一样,举例来说,0644表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存
+③ return:成功时返回一个与key相关的共享内存标识符(非负整数)。调用失败返回-1
+不相关的进程可以返回值(共享内存标识符)访问同一共享内存。
+2.shmat
第一次创建完共享内存时,它还不能被任何进程访问,需要shmat启动对该共享内存的访问,并把共享内存连接到当前进程的地址空间。
+-
void *shmat(int shm_id, const void *shm_addr, int shmflg);大概buf数组里存储着所有buf的内容。buf本身通过最近使用排序的双向链表连接,head是链表的头。
-初始化
+
// called by main.c
void
binit(void)
{
struct buf *b;
initlock(&bcache.lock, "bcache");
// Create linked list of buffers
// 把b插在head之后
bcache.head.prev = &bcache.head;
bcache.head.next = &bcache.head;
for(b = bcache.buf; b < bcache.buf+NBUF; b++){
b->next = bcache.head.next;
b->prev = &bcache.head;
initsleeplock(&b->lock, "buffer");
bcache.head.next->prev = b;
bcache.head.next = b;
}
}① shm_id:共享内存标识符
+② shm_addr:指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址
+③ shm_flg:一组标志位,通常为0
+④ return:成功时返回一个指向共享内存第一个字节的指针,失败返回-1
+3.shmdt
用于将共享内存从当前进程中分离,使该共享内存对当前进程不再可用。
+-
int shmdt(const void *shmaddr);上层接口
--The main interface exported by the buffer cache consists of
-bread
andbwrite
.The buffer cache uses a per-buffer sleep-lock to ensure concurrent security.
-bread
---
bread
obtains a buf containing a copy of a block which can be read or modified in memory.依据给定设备号和给定扇区号寻找cache的buf。返回的buf是locked的。
-+
// Return a locked buf with the contents of the indicated block.
struct buf*
bread(uint dev, uint blockno)
{
struct buf *b;
// 获取buf块
b = bget(dev, blockno);
if(!b->valid) {
// 说明cache未命中,需要从磁盘读入
virtio_disk_rw(b, 0);
b->valid = 1;
}
return b;
}① shmaddr:shmat返回的共享内存指针
+② return:成功0,失败1
+4.shmctl
用来控制共享内存
+-
int shmctl(int shm_id, int command, struct shmid_ds *buf);bwrite
--writes a modified buffer to the appropriate block on the disk
-+
// Write b's contents to disk. Must be locked.
void
bwrite(struct buf *b)
{
// 必须持有b的锁
if(!holdingsleep(&b->lock))
panic("bwrite");
// 写入磁盘
virtio_disk_rw(b, 1);
}① shm_id:共享内存标识符
+② command:要采取的操作,它可以取下面的三个值 :
++
+- IPC_STAT:把shmid_ds结构中的数据设置为共享内存的当前关联值,即用共享内存的当前关联值覆盖shmid_ds的值。
+- IPC_SET:如果进程有足够的权限,就把共享内存的当前关联值设置为shmid_ds结构中给出的值
+- IPC_RMID:删除共享内存段
+③ buf:结构指针
+shmid_ds结构 至少包括以下成员:
+-
struct shmid_ds
{
uid_t shm_perm.uid;
uid_t shm_perm.gid;
mode_t shm_perm.mode;
};brelse
-A kernel thread must release a buffer by calling brelse when it is done with it.
+实验1 在Ubuntu下编写程序“基于共享内存的生产者消费者模型”
+-本项实验在 Ubuntu 下完成,与信号量实验中的
+pc.c
的功能要求基本一致,仅有两点不同:+
+- 不用文件做缓冲区,而是使用共享内存;
+- 生产者和消费者分别是不同的程序。生产者是 producer.c,消费者是 consumer.c。两个程序都是单进程的,通过信号量和缓冲区进行通信。
+Linux 下,可以通过
shmget()
和shmat()
两个系统调用使用共享内存。+
// Release a locked buffer.
// Move to the head of the most-recently-used list.
void
brelse(struct buf *b)
{
if(!holdingsleep(&b->lock))
panic("brelse");
releasesleep(&b->lock);
acquire(&bcache.lock);
b->refcnt--;
if (b->refcnt == 0) {
// no one is waiting for it.
// 移动到头结点和头结点的下一个结点之间的位置
b->next->prev = b->prev;
b->prev->next = b->next;
b->next = bcache.head.next;
b->prev = &bcache.head;
bcache.head.next->prev = b;
bcache.head.next = b;
}
release(&bcache.lock);
}直接上代码。感觉比文件操作简单多了2333
+consumer.c
+-
sem_t* empty;
sem_t* full;
sem_t* mutex;
char* p;
void Consumer(){
sem_wait(full);
sem_wait(mutex);
char s[5]={0};
//there read s from buffer...
memcpy(s,p,sizeof(char)*4);
p+=4;
printf("%d : %s",getpid(),s);
sem_post(mutex);
sem_post(empty);
}
int main(){
empty=sem_open("empty",O_CREAT,0644,10);
full=sem_open("full",O_CREAT,0644,0);
mutex=sem_open("mutex",O_CREAT,0644,1);
int shm_id=shmget(521,sizeof(char)*4*600,0644|IPC_CREAT);
char* shm=(char*)shmat(shm_id,NULL,0);
p=shm;
int cnt=500;
while(cnt--){
Consumer();
}
shmdt(shm);
shmctl(shm_id,IPC_RMID,0);
sem_unlink("mutex");
sem_unlink("full");
sem_unlink("empty");
return 0;
}具体细节
bget
用于获取cache中是否存在block。如果不存在,则新申请一个buf,并把该buf以上锁状态返回
-+
// Look through buffer cache for block on device dev.
// If not found, allocate a buffer.
// In either case, return locked buffer.
static struct buf*
bget(uint dev, uint blockno)
{
struct buf *b;
acquire(&bcache.lock);
// Is the block already cached?
// 这个循环条件很有意思,充分用到了双向链表的特性
for(b = bcache.head.next; b != &bcache.head; b = b->next){
if(b->dev == dev && b->blockno == blockno){
// 引用数增加
b->refcnt++;
release(&bcache.lock);
// 锁定
acquiresleep(&b->lock);
return b;
}
}
// Not cached.
// Recycle the least recently used (LRU) unused buffer.
// 从尾部开始遍历,确实就是最少使用的了
for(b = bcache.head.prev; b != &bcache.head; b = b->prev){
// 如果该buf空闲
if(b->refcnt == 0) {
b->dev = dev;
b->blockno = blockno;
// 仅是新建了一个buf,还未从磁盘读取对应磁盘块的副本,因而设valid为0以供上层函数调用处理
b->valid = 0;
b->refcnt = 1;
release(&bcache.lock);
// 锁定
acquiresleep(&b->lock);
return b;
}
}
// cache不够用了
panic("bget: no buffers");
}producer.c
+-
sem_t* empty;
sem_t* full;
sem_t* mutex;
char* p;
void Producer(){
int i;
char s[5]={0};
for(i=0;i<=500;i++){
sem_wait(empty);
sem_wait(mutex);
sprintf(s,"%03d\n",i);
//there write s into buffer...
memcpy(p,s,sizeof(char)*4);
p+=4;
printf("Write into success!%s\n",s);
sem_post(mutex);
sem_post(full);
}
}
int main(){
empty=sem_open("empty",O_CREAT,0644,10);
full=sem_open("full",O_CREAT,0644,0);
mutex=sem_open("mutex",O_CREAT,0644,1);
int shm_id=shmget(521,sizeof(char)*4*600,0644|IPC_CREAT);
char* shm=(char*)shmat(shm_id,NULL,0);
p=shm;
Producer();
shmdt(shm);
shmctl(shm_id,IPC_RMID,0);
sem_unlink("mutex");
sem_unlink("full");
sem_unlink("empty");
return 0;
}Logging layer
简介
--Xv6通过简单的日志记录形式解决了文件系统操作期间的崩溃问题。
-xv6系统调用不会直接写入磁盘上的文件系统数据结构。相反,它会在磁盘上的log(日志)中放置它希望进行的所有磁盘写入的描述。一旦系统调用记录了它的所有写入操作,它就会向磁盘写入一条特殊的commit(提交)记录,表明日志包含一个完整的操作。此时,系统调用将写操作复制到磁盘上的文件系统数据结构。完成这些写入后,系统调用将擦除磁盘上的日志。
---如果系统崩溃并重新启动,则在运行任何进程之前,文件系统代码将按如下方式从崩溃中恢复:
-如果日志标记为包含完整操作,则恢复代码会将写操作复制到磁盘文件系统中它们所属的位置,然后擦除日志。如果日志没有标记为包含完整操作,则恢复代码将忽略该日志,然后擦除日志。
-这就保证了原子性。
-Log design
-
superblock记录了log的存储位置。
---它由一个头块(header block)和一系列更新块的副本(logged block)组成。
-头块包含一个扇区号(sector)数组(每个logged block对应一个扇区号)以及日志块的计数。
-磁盘上的头块中的计数为零表示日志中没有事务,为非零表示日志包含一个完整的已提交事务,并具有指定数量的logged block。
-在事务提交(commit)时Xv6才向头块写入数据,在此之前不会写入。在将logged blocks复制到文件系统后,头块的计数将被设置为零。
-因此,事务中途崩溃将导致日志头块中的计数为零;提交后的崩溃将导致非零计数。
---为了允许不同进程并发执行文件系统操作,日志系统可以将多个系统调用的写入累积到一个事务中。因此,单个提交可能涉及多个完整系统调用的写入。为了避免在事务之间拆分系统调用,日志系统仅在没有文件系统调用进行时提交。
-同时提交多个事务的想法称为组提交(group commit)。组提交减少了磁盘操作的数量,因为成本固定的一次提交分摊了多个操作。组提交还同时为磁盘系统提供更多并发写操作,可能允许磁盘在一个磁盘旋转时间内写入所有这些操作。Xv6的virtio驱动程序不支持这种批处理,但是Xv6的文件系统设计允许这样做。
-【这感觉实现得也还挺简略的】
---Xv6在磁盘上留出固定的空间来保存日志。事务中系统调用写入的块总数必须可容纳于该空间。这导致两个后果:
--
-- -
任何单个系统调用都不允许写入超过日志空间的不同块。
-【这段话我一个字没看懂】
-这对于大多数系统调用来说都不是问题,但其中两个可能会写入许多块:
-write
和unlink
。一个大文件的write
可以写入多个数据块和多个位图块以及一个inode块;unlink
大文件可能会写入许多位图块和inode。Xv6的write
系统调用将大的写入分解为适合日志的多个较小的写入,unlink
不会导致此问题,因为实际上Xv6文件系统只使用一个位图块。- -
日志空间有限的另一个后果是,除非确定系统调用的写入将可容纳于日志中剩余的空间,否则日志系统无法允许启动系统调用。
-Code: logging
--log的原理是这样的:
-在每个系统调用的开始调用
-begin_op
表示事务开始,然后之后新申请一块block,也即把该block的内容读入内存,并且把该block的blockno记录到log的header中。此后程序正常修改在内存中的block,磁盘中的block保持不变。最后commit的时候遍历log header中的blockno,一块块地把内存中的block写入日志和磁盘中。如果程序在commit前崩溃,则内存消失,同时磁盘也不会写入;如果在commit后崩溃,那也无事发生。
-在每次启动的时候,都会执行log的初始化,届时可以顺便恢复数据。
-完美实现了日志的功能。
--
数据结构
+
// Contents of the header block, used for both the on-disk header block
// and to keep track in memory of logged block# before commit.
struct logheader {
int n;
// 扇区号也即blockno的数组
int block[LOGSIZE];
};
// 代表log磁盘块
struct log {
struct spinlock lock;
int start;// log磁盘块的开始。start开始的第一块为log header,之后皆为写入的block
int size;
int outstanding; // how many FS sys calls are executing.
int committing; // in commit(), please wait.
int dev;
struct logheader lh;
};
struct log log;编译运行指令
+-
gcc -o producer producer.c -pthread
gcc -o consumer consumer.c -pthread
./producer > p.txt &
./consumer > c.txt关键函数
begin_op()
--
begin_op
等待直到日志系统当前未处于提交中,并且直到有足够的未被占用的日志空间来保存此调用的写入。+
log.outstanding
统计预定了日志空间的系统调用数;为此保留的总空间为log.outstanding
乘以MAXOPBLOCKS
(10)。递增log.outstanding
会预定空间并防止在此系统调用期间发生提交(if的第二个分支)。代码保守地假设每个系统调用最多可以写入MAXOPBLOCKS
(10)个不同的块。运行结果c.txt(仅展示部分)
++ + + +
27696 : 000
27696 : 001
27696 : 002
27696 : 003
27696 : 004
27696 : 005
27696 : 006
27696 : 007
27696 : 008
27696 : 009
27696 : 010
27696 : 011
27696 : 012实验2 在Linux0.11实现共享内存
+-进程之间可以通过页共享进行通信,被共享的页叫做共享内存,结构如下图所示:
++ +
本部分实验内容是在 Linux 0.11 上实现上述页面共享,并将上一部分实现的 producer.c 和 consumer.c 移植过来,验证页面共享的有效性。
+
// called at the start of each FS system call.
void
begin_op(void)
{
acquire(&log.lock);
while(1){
// 正在提交则等待日志空闲
if(log.committing){
sleep(&log, &log.lock);
// 日志空间不足则等待空间充足
} else if(log.lh.n + (log.outstanding+1)*MAXOPBLOCKS > LOGSIZE){
// this op might exhaust log space此操作可能会耗尽日志空间; wait for commit.
sleep(&log, &log.lock);
} else {
log.outstanding += 1;
release(&log.lock);
break;
}
}
}+具体要求在
+mm/shm.c
中实现shmget()
和shmat()
两个系统调用。它们能支持producer.c
和consumer.c
的运行即可,不需要完整地实现 POSIX 所规定的功能。+
+- shmget()
+-
int shmget(key_t key, size_t size, int shmflg);log_write
---
log_write
充当bwrite
的代理。它将块的扇区号记录在内存中,在磁盘上的日志中预定一个槽位,并调用bpin
将缓存固定在block cache中,以防止block cache将其逐出【具体原理就是让refcnt++,这样就不会被当成空闲block用掉了】。为啥要防止换出呢?换出不是就正好自动写入磁盘了吗?这里一是为了保障前面提到的原子性,防止换入换出导致的单一写入磁盘;二是换出自动写入的是磁盘对应位而不一定是日志所在的blocks。
++
shmget()
会新建/打开一页内存,并返回该页共享内存的 shmid(该块共享内存在操作系统内部的 id)。所有使用同一块共享内存的进程都要使用相同的 key 参数。
+如果 key 所对应的共享内存已经建立,则直接返回
+shmid
。如果 size 超过一页内存的大小,返回-1
,并置errno
为EINVAL
。如果系统无空闲内存,返回 -1,并置errno
为ENOMEM
。+
shmflg
参数可忽略。+
+- shmat()
++ +
void *shmat(int shmid, const void *shmaddr, int shmflg);+
shmat()
会将shmid
指定的共享页面映射到当前进程的虚拟地址空间中,并将其首地址返回。如果
+shmid
非法,返回-1
,并置errno
为EINVAL
。
shmaddr
和shmflg
参数可忽略。+
void
log_write(struct buf *b)
{
int i;
// #define LOGSIZE (MAXOPBLOCKS*3) // max data blocks in on-disk log
// 30
if (log.lh.n >= LOGSIZE || log.lh.n >= log.size - 1)
panic("too big a transaction");
if (log.outstanding < 1)
panic("log_write outside of trans");
acquire(&log.lock);
// log_write会注意到在单个事务中多次写入一个块的情况,并在日志中为该块分配相同的槽位。
// 这种优化通常称为合并(absorption)
for (i = 0; i < log.lh.n; i++) {
if (log.lh.block[i] == b->blockno) // log absorbtion
break;
}
// 这里还是挺巧妙的。
// 如果存在log.lh.block[i] == b->blockno的情况,执行此句话也无妨
// 如果不存在,则给log新增一块,填入log.lh.block[log.lh.n]的位置,再++log.lh.n
log.lh.block[i] = b->blockno;
if (i == log.lh.n) { // Add new block to log?
bpin(b);
log.lh.n++;
}
release(&log.lock);
}思路:
+1.shmget:由其论述,我们可以知道,我们需要建立一个映射表,其中成员为结构体({key_t key,size_t size,unsigned long page}),每次只需查找映射表,如果有对应key则返回下标,如果没有则新建页表,填入映射体,再返回对应下标。
+以下为了图省事,对映射表的实现进行了简化,把key直接当做int类型,作为映射表下标,映射表成员为page,unsigned long。
+2.shmat:
+首先由指导书的提示:
+-
// 建立线性地址和物理地址的映射
put_page(tmp, address);end_op
+
// called at the end of each FS system call.
// 如果这是最后一层outstanding就会执行commit操作
// commits if this was the last outstanding operation.
void
end_op(void)
{
int do_commit = 0;
acquire(&log.lock);
log.outstanding -= 1;
if(log.committing)
panic("log.committing");
if(log.outstanding == 0){
do_commit = 1;
log.committing = 1;
} else {
// begin_op() may be waiting for log space,
// and decrementing log.outstanding has decreased
// the amount of reserved space.
wakeup(&log);
}
release(&log.lock);
if(do_commit){
// call commit w/o holding locks, since not allowed
// to sleep with locks.
commit();
acquire(&log.lock);
log.committing = 0;
wakeup(&log);
release(&log.lock);
}
}我们知道在shmat中,要建立shmget得到的共享物理页面与其虚拟地址的映射,就需要使用这个put_page函数。
+但是put_page函数的参数为页和address。页就是我们的shm_map[key],address=虚拟地址+段基址。那么如何得到虚拟地址呢?
+通过指导书的提示:
+-
code_base = get_base(current->ldt[1]);
data_base = code_base;
// 数据段基址 = 代码段基址
set_base(current->ldt[1],code_base);
set_limit(current->ldt[1],code_limit);
set_base(current->ldt[2],data_base);
set_limit(current->ldt[2],data_limit);
__asm__("pushl $0x17\n\tpop %%fs":: );
// 从数据段的末尾开始
data_base += data_limit;
// 向前处理
for (i=MAX_ARG_PAGES-1 ; i>=0 ; i--) {
// 一次处理一页
data_base -= PAGE_SIZE;
// 建立线性地址到物理页的映射
if (page[i]) put_page(page[i],data_base);
}commit
+
static void
commit()
{
if (log.lh.n > 0) {
// cache -> log block
write_log(); // Write modified blocks from cache to log
// head(in stack/heap) -> log block
// 此可以说为commit完成的标志。
// 因为无论接下来是否崩溃,数据最终都会被写入disk,不同在于是在recover时还是接下来写入
write_head(); // Write header to disk -- the real commit
// log block -> real position
install_trans(0); // Now install writes to home locations
log.lh.n = 0;
// 擦除
write_head(); // Erase the transaction from the log
}
}再结合所学知识,我们可以知道几点:
+① 数据段的基址可由current->ldt[2]给出 ② address=虚拟地址+段基址 ③ 我们需要分配给当前共享内存一段空闲的虚拟地址段
+则该小段空闲数据段的虚拟地址就是我们的return值,address=return+data_base。
+问题就转化成了如何获取一段空闲数据段。
+我们由下图:
+-
write_log
+
// Copy modified blocks from cache to log.
static void
write_log(void)
{
int tail;
for (tail = 0; tail < log.lh.n; tail++) {
struct buf *to = bread(log.dev, log.start+tail+1); // log block
struct buf *from = bread(log.dev, log.lh.block[tail]); // cache block
memmove(to->data, from->data, BSIZE);
bwrite(to); // write the log
brelse(from);// 此处的brelse呼应了外界调用的bread
brelse(to);
}
}可知,brk指针指向堆区顶部,即空闲堆的起始位置。因而我们可以用这段空间作为我们要的空闲数据段,当前brk即为虚拟地址。
+我们的页有PAGE_SIZE那么大,因而自然也就要用PAGE_SIZE那么大的空闲数据段了。
+解说完毕,以下上代码~
+-
unsigned long shm_map[600];
int sys_shmget(int key,size_t size,int shmflg){
if(key<0||key>=600) return -1;
if(shm_map[key]!=0) return key;
unsigned long tmp=get_free_page();
shm_map[key]=tmp;
return key;
}
void* sys_shmat(int shmid,const void* shmaddr,int shmflg){
if(shmid<0||shmid>=600) return -1;
unsigned long page=shm_map[shmid];
//得到数据段的基址
unsigned long data_base=get_base(current->ldt[2]);
//brk指针指向空闲数据段的开始
unsigned long brk=current->brk+data_base;
current->brk+=PAGE_SIZE;
//建立内存映射
put_page(page,brk);
return (void*)(brk-data_base);
}write_head
+
// Write in-memory log header to disk.
// 这是事务提交的标志
// This is the true point at which the
// current transaction commits.
static void
write_head(void)
{
struct buf *buf = bread(log.dev, log.start);
struct logheader *hb = (struct logheader *) (buf->data);
int i;
hb->n = log.lh.n;
for (i = 0; i < log.lh.n; i++) {
hb->block[i] = log.lh.block[i];
}
bwrite(buf);
brelse(buf);
}信号量
任务一 实现pc.c
-
在 Ubuntu 上编写应用程序“pc.c”,解决经典的生产者—消费者问题,完成下面的功能:
1.建立一个生产者进程,N 个消费者进程(N>1);
2.用文件建立一个共享缓冲区;
3.生产者进程依次向缓冲区写入整数 0,1,2,...,M,M>=500;
4.消费者进程从缓冲区读数,每次读一个,并将读出的数字从缓冲区删除,然后将本进程 ID 和 + 数字输出到标准输出;
5.缓冲区同时最多只能保存 10 个数。
其中 ID 的顺序会有较大变化,但冒号后的数字一定是从 0 开始递增加一的。install_trans
+
// Copy committed blocks from log to their home location
static void
install_trans(int recovering)
{
int tail;
for (tail = 0; tail < log.lh.n; tail++) {
struct buf *lbuf = bread(log.dev, log.start+tail+1); // read log block
struct buf *dbuf = bread(log.dev, log.lh.block[tail]); // read dst
memmove(dbuf->data, lbuf->data, BSIZE); // copy block to dst
bwrite(dbuf); // write dst to disk
if(recovering == 0)
bunpin(dbuf);// 如果不是在recover的过程中
brelse(lbuf);
brelse(dbuf);
}
}先附上我的代码吧【注:我没做到从缓冲区删除,但其他都完成了】
+-
const char* filename="buffer.txt";
sem_t* empty;
sem_t* full;
sem_t* mutex;
int fd;
void Producer(){
int i;
char s[5]={0};
for(i=0;i<=500;i++){
sem_wait(empty);
sem_wait(mutex);
int tmp=lseek(fd,0,SEEK_CUR);
lseek(fd,0,SEEK_END);
sprintf(s,"%03d\n",i);
write(fd,s,4);
lseek(fd,tmp,SEEK_SET);
sem_post(mutex);
sem_post(full);
}
}
void Consumer(){
sem_wait(full);
sem_wait(mutex);
char s[5]={0};
read(fd,s,4);
printf("%d : %s",getpid(),s);
sem_post(mutex);
sem_post(empty);
}
int main(){
sem_unlink("empty");
sem_unlink("full");
sem_unlink("mutex");
fd=open(filename,O_RDWR|O_CREAT);
printf("%d\n",errno);
empty=sem_open("empty",O_CREAT,0644,10);
full=sem_open("full",O_CREAT,0644,0);
mutex=sem_open("mutex",O_CREAT,0644,1);
if(!fork()){
Producer();
return 0;
}
int i;
for(i=0;i<10;i++){
if(!fork()){
while(1) Consumer();
}
}
close(fd);
return 0;
}恢复与初始化
上面介绍了log的一次事务提交的流程。接下来介绍它是怎么恢复的。
----
recover_from_log
是由initlog
调用的,而它又是在第一个用户进程运行之前的引导期间由fsinit
调用的。第一个进程运行之前
由前面scheduler一章的知识可知,每个进程被初次调度的时候会先来执行
-forkret
。这时候就做了log的恢复工作。注释解释了为什么不选择在
-main.c
中初始化,而选择在此处初始化。确实,它需要调用sleep,如果在main.c中调用sleep感觉会乱套()毕竟那时候scheduler线程尚未被初始化。+
// A fork child's very first scheduling by scheduler()
// will swtch to forkret.
void
forkret(void)
{
// static变量仅会被初始化一次
static int first = 1;
// Still holding p->lock from scheduler.
release(&myproc()->lock);
// 如果是第一个进程
if (first) {
// File system initialization must be run in the context of a
// regular process (e.g., because it calls sleep), and thus cannot
// be run from main().
first = 0;
fsinit(ROOTDEV);
}
usertrapret();
}运行效果:
+-
fsinit
+
// Init fs
void
fsinit(int dev) {
// ...
initlog(dev, &sb);
}要点1 系统调用的IO读写
这部分耗费了我海量时间,主要原因还是因为我没有好好学就直接上手写导致很多地方都因为不清楚而寄了。。。
+先大致讲讲文件读写的原理吧。打开一个文件作为数据流,有一个文件指针,该指针指向的地方就是之后读写开始的地方,读写还有lseek都可以让指针移动。
+再放个各个系统调用的签名。
+-
@param 文件名 模式
@return 所需文件描述符
int open(char* filename,int flag);initlog
+
void
initlog(int dev, struct superblock *sb)
{
if (sizeof(struct logheader) >= BSIZE)
panic("initlog: too big logheader");
initlock(&log.lock, "log");
// 从super block中获取必要参数
log.start = sb->logstart;
log.size = sb->nlog;
log.dev = dev;
recover_from_log();
}其中flag的可能取值:
+-
recover_from_log
+
static void
recover_from_log(void)
{
// 读取head
read_head();
// 注意,commit中会把header写入log block,而这里从log block读出header
// 也就是说,如果header的n不为零,那么说明已经commit了,但可能未写入,重复写入保障安全
// 如果header的n为零,说明未commit,在install_trans的逻辑中会什么也不做
// 两种情况完美满足
install_trans(1); // if committed, copy from log to disk
log.lh.n = 0;
// 擦除
write_head(); // clear the log
}-
Code: Block allocator
个人理解
说实话没怎么懂,也不大清楚它有什么用,先大概推测一下:
-之前的bread和bwrite这些,就是你给一个设备号和扇区号,它就帮你加载进内存cache。你如果要用的话,肯定还是使用地址方便。所以block allocator的作用之一就是给bread和bwrite加一层封装,将获取的block封装为地址返回,你可以直接操纵这个地址,而无需知道下层的细节。
-这个过程要注意的有两点:
--
-- -
封装返回的地址具体是什么,怎么工作的
-封装返回的地址实质上是buffer cache中的buf的data字段的地址【差不多】。之后的上层应用在该地址上写入,也即写入了buf,最后会通过log层真正写入磁盘。
-- -
结合bcache的LRU,详细谈谈工作机制
-我们可以看到,在balloc中有这么一段逻辑:
-+
bp = bread(dev, BBLOCK(b, sb));
// ...
log_write(bp);
brelse(bp);
return b + bi;如果想要多个方式并行,则可以用|连接。【联系一下原理,这大概是用了标志位吧,每个标志只有一位是1】
+这部分踩过的坑:
+① 选择O_CREAT,如果文件已经存在,居然是会报错?【表现为errno=13,还会输出一堆奇怪的东西】
+-
@param 文件描述符 写入字符串 写入长度
@return 报错信息
int read(int fd,char* string,size_t size);看到的第一反应就是,我们需求的那块buf是bp,但是这里先是bread了一次,又是brelse了一次,这样bp的refcnt不就为0,很容易被替换掉了吗?
-会有这个反应,一定程度上是因为没有很好地理解LRU。事实上,正是它可能被替换掉,才满足了LRU的条件。因为它可能被替掉才能说明它可能是最近最少使用的。
-bitmap
--文件和目录内容存储在磁盘块中,磁盘块必须从空闲池中分配。xv6的块分配器在磁盘上维护一个空闲位图,每一位代表一个块。0表示对应的块是空闲的;1表示它正在使用中。
-引导扇区、超级块、日志块、inode块和位图块的比特位是由程序
-mkfs
初始化设置的:-
allocator
类似于memory allocator,块分配器也提供了两个函数:
-bfree
和balloc
。balloc
---
Balloc
从块0到sb.size
(文件系统中的块数)遍历每个块。它查找位图中位为零的空闲块。如果balloc
找到这样一个块,它将更新位图并返回该块。为了提高效率,循环被分成两部分。外部循环读取位图中的每个块。内部循环检查单个位图块中的所有BPB位。由于任何一个位图块在buffer cache中一次只允许一个进程使用【
-bread(dev, BBLOCK(b, sb))
会返回一个上锁的block,bread
和brelse
隐含的独占使用避免了显式锁定的需要】,因此,如果两个进程同时尝试分配一个块也是并发安全的。+
// Allocate a zeroed disk block.
static uint
balloc(uint dev)
{
int b, bi, m;
struct buf *bp;
bp = 0;
for(b = 0; b < sb.size; b += BPB){
bp = bread(dev, BBLOCK(b, sb));
for(bi = 0; bi < BPB && b + bi < sb.size; bi++){
m = 1 << (bi % 8);
if((bp->data[bi/8] & m) == 0){ // Is block free?
bp->data[bi/8] |= m; // Mark block in use.
log_write(bp);
brelse(bp);
bzero(dev, b + bi);
return b + bi;
}
}
brelse(bp);
}
panic("balloc: out of blocks");
}read会读出size个字节然后存进string里面,同时也会移动文件指针向前size个字节。
+-
@param 文件描述符 写入字符串 写入长度
@return 报错信息
int write(int fd,char* string,size_t size);bfree
+
// Free a disk block.
static void
bfree(int dev, uint b)
{
struct buf *bp;
int bi, m;
bp = bread(dev, BBLOCK(b, sb));
bi = b % BPB;
m = 1 << (bi % 8);
if((bp->data[bi/8] & m) == 0)
panic("freeing free block");
bp->data[bi/8] &= ~m;
log_write(bp);
brelse(bp);
}基本同write。
+这部分踩过的坑:
+write(fd,NULL,0) ——合法
+write(fd,NULL,a),a>0 ——寄!
+这还是因为write的具体实现了。
+write里面有个判断
+-
int sys_write(unsigned int fd,char *buf,int count){
//...
if(!count) return 0;
//...
while(c-->0) *(p++)=get_fs_byte(buf++);
}Inode layer
inode
--术语inode(即索引结点)可以具有两种相关含义之一。它可能是指包含文件大小和数据块编号列表的磁盘上的数据结构【on-disk inode】。或者“inode”可能指内存中的inode【in-memory inode】,它包含磁盘上inode的副本以及内核中所需的额外信息。
--
on-disk inode
--The on-disk inodes are packed into a contiguous area of disk called the inode blocks.
-Every inode is the same size, so it is easy, given a number n, to find the nth inode on the disk. In fact, this number n, called the inode number or i-number, is how inodes are identifified in the implementation.
-+
// in fs.h
// On-disk inode structure
struct dinode {
// 为0表示free
short type; // File type
short major; // Major device number (T_DEVICE only)
short minor; // Minor device number (T_DEVICE only)
// The nlink field counts the number of directory entries that refer to this inode,
// in order to recognize when the on-disk inode and its data blocks should be freed.
short nlink; // Number of links to inode in file system
uint size; // Size of file (bytes)
uint addrs[NDIRECT+1]; // Data block addresses
};而get_fs_byte:
+-
in-memory inode
--The kernel keeps the set of active inodes in memory.
-The kernel stores an inode in memory only if there are C pointers referring to that inode.当且仅当ref==0才会从内核中释放。
-如果nlinks==0就会从物理block中释放。
-The
-iget
andiput
functions acquire and release pointers to an inode, modifying the reference count.【相当于buffer cache的balloc
和bfree
】Pointers to an inode can come from file descriptors, current working directories, and transient kernel code such as exec.-
iget
返回的struct inode
可能没有任何有用的内容。为了确保它保存磁盘inode的副本,代码必须调用ilock
。这将锁定inode(以便没有其他进程可以对其进行ilock
),并从磁盘读取尚未读取的inode。iunlock
释放inode上的锁。将inode指针的获取与锁定分离有助于在某些情况下避免死锁,例如在目录查找期间。多个进程可以持有指向iget
返回的inode的C指针,但一次只能有一个进程锁定inode。+
//in file.h
// in-memory copy of an inode
struct inode {
uint dev; // Device number
uint inum; // Inode number
int ref; // Reference count
struct sleeplock lock; // protects everything below here
int valid; // inode has been read from disk?
short type; // copy of disk inode
short major;
short minor;
short nlink;
uint size;
uint addrs[NDIRECT+1];// 存储着inode数据的blocks的地址,从balloc中获取
};确实感觉空的话挺危险的【】
+-
@param 文件描述符
@return 报错信息
int close(fd);Code: inode
--主要是在讲inode layer这一层的方法,以及给上层提供的接口。
-Overview
-
底层接口
---
iget
iput
iget
逻辑还是跟buffer cache非常相似的,不过可以看出这个的数据结构简单许多,也不用实现LRU。
---A struct inode pointer returned by iget() is guaranteed to be valid until the corresponding call to iput(): the inode won’t be deleted, and the memory referred to by the pointer won’t be re-used for a different inode. 【通过ref++实现。】
-不同于buffer cache的
-bget
,iget()
提供对inode的非独占访问,因此可以有许多指向同一inode的指针。文件系统代码的许多部分都依赖于iget()
的这种行为,既可以保存对inode的长期引用(如打开的文件和当前目录),也可以防止争用,同时避免操纵多个inode(如路径名查找)的代码产生死锁。+
// Find the inode with number inum on device dev
// and return the in-memory copy. Does not lock
// the inode and does not read it from disk.
static struct inode*
iget(uint dev, uint inum)
{
struct inode *ip, *empty;
acquire(&icache.lock);
// Is the inode already cached?
empty = 0;
for(ip = &icache.inode[0]; ip < &icache.inode[NINODE]; ip++){
if(ip->ref > 0 && ip->dev == dev && ip->inum == inum){
ip->ref++;
release(&icache.lock);
return ip;
}
// 由于不用实现LRU,所以只需一次循环记录即可。
if(empty == 0 && ip->ref == 0) // Remember empty slot.
empty = ip;
}
// Recycle an inode cache entry.
if(empty == 0)
panic("iget: no inodes");
ip = empty;
ip->dev = dev;
ip->inum = inum;
ip->ref = 1;
// does not read from disk
ip->valid = 0;
release(&icache.lock);
return ip;
}这个没啥好说的,记得关就是了
+要点2 信号量的调用
这方面看linux自带的man文档就行,写得很清楚。
+输入指令:
+-
man sem_overviewiput
---
iput()
可以写入磁盘。这意味着任何使用文件系统的系统调用都可能写入磁盘,因为系统调用可能是最后一个引用该文件的系统调用。即使像read()
这样看起来是只读的调用,也可能最终调用iput()
。这反过来意味着,即使是只读系统调用,如果它们使用文件系统,也必须在事务中进行包装。-
iput()
和崩溃之间存在一种具有挑战性的交互。iput()
不会在文件的链接计数降至零时立即截断文件,因为某些进程可能仍在内存中保留对inode的引用:进程可能仍在读取和写入该文件,因为它已成功打开该文件。但是,如果在最后一个进程关闭该文件的文件描述符之前发生崩溃,则该文件将被标记为已在磁盘上分配,但没有目录项指向它。如果不做任何处理措施的话,这块磁盘就再也用不了了。文件系统以两种方式之一处理这种情况。简单的解决方案用于恢复时:重新启动后,文件系统会扫描整个文件系统,以查找标记为已分配但没有指向它们的目录项的文件。如果存在任何此类文件,接下来可以将其释放。
-第二种解决方案不需要扫描文件系统。在此解决方案中,文件系统在磁盘(例如在超级块中)上记录链接计数降至零但引用计数不为零的文件的i-number。如果文件系统在其引用计数达到0时删除该文件,则会通过从列表中删除该inode来更新磁盘列表。重新启动时,文件系统将释放列表中的所有文件。
-Xv6没有实现这两种解决方案,这意味着inode可能被标记为已在磁盘上分配,即使它们不再使用。这意味着随着时间的推移,xv6可能会面临磁盘空间不足的风险。
-+
// Drop a reference to an in-memory inode.
// If that was the last reference, the inode cache entry can
// be recycled.【refvnt==0 可以回收】
// 注意这个回收过程无需特别处理,只需自然--refcnt就行,不用像buffer cache那么烦
// If that was the last reference and the inode has no links
// to it, free the inode (and its content) on disk.【nlinks==0 copy和本体都得扔掉】
// All calls to iput() must be inside a transaction in
// case it has to free the inode.任何需要iput的地方都需要包裹在事务内,因为它可能会释放inode
void
iput(struct inode *ip)
{
acquire(&icache.lock);
if(ip->ref == 1 && ip->valid && ip->nlink == 0){
// inode has no links and no other references: truncate and free.
// ip->ref == 1 means no other process can have ip locked,
// so this acquiresleep() won't block (or deadlock).
acquiresleep(&ip->lock);
release(&icache.lock);
// 最终调用bfree,会标记bitmap,完全释放block
itrunc(ip);
ip->type = 0;
/*iupdate:
// Copy a modified in-memory inode to disk.
// Must be called after every change to an ip->xxx field
// that lives on disk, since i-node cache is write-through.
write-through:
CPU向cache写入数据时,同时向memory(后端存储)也写一份,使cache和memory的数据保持一致。
*/
// 这里修改的type是dinode也有的字段,所以需要update一下。
// 下面的valid是dinode没有的字段,所以随便改,无需update
iupdate(ip);
ip->valid = 0;
releasesleep(&ip->lock);
acquire(&icache.lock);
}
ip->ref--;
release(&icache.lock);
}这部分踩过的坑:
+千万注意最后不使用信号量时要释放,使用sem_unlink。不然最后的输出结果会非常诡异。
+要点3 编写程序
以上差不多就是涉及到的需要自己了解的课外知识点了,接下来就需要自己编写程序。
+总体框架就按它给的差不多:
+-
Producer()
{
// 空闲缓存资源
P(Empty);
// 互斥信号量
P(Mutex);
//生产并放一个item进缓冲区
V(Mutex);
V(Full);
}
Consumer()
{
P(Full);
P(Mutex);
//从缓存区取出一个赋值给item并消费;
V(Mutex);
V(Empty);
}上层接口
获取和释放inode
ialloc
+
// Allocate an inode on device dev.
// Mark it as allocated by giving it type type.
// Returns an unlocked but allocated and referenced inode.
struct inode*
ialloc(uint dev, short type)
{
int inum;
struct buf *bp;
struct dinode *dip;
for(inum = 1; inum < sb.ninodes; inum++){
bp = bread(dev, IBLOCK(inum, sb));
dip = (struct dinode*)bp->data + inum%IPB;
if(dip->type == 0){ // a free inode通过type判断是否free
memset(dip, 0, sizeof(*dip));// zerod
dip->type = type;
log_write(bp); // mark it allocated on the disk
brelse(bp);
return iget(dev, inum);
}
brelse(bp);
}
panic("ialloc: no inodes");
}有个点挺有趣的,就是它实际上把文件指针也看成一种资源了,因此也需要在同步段对其进行更新。
+printf的stdout也是资源。
+故以上两者都只能在锁内同步段进行更新。
+main函数就照本宣科地用fork建立子进程就行。
+任务二 自己实现信号量
-
Linux 在 0.11 版还没有实现信号量,Linus 把这件富有挑战的工作留给了你。如果能实现一套山寨版的完全符合 POSIX 规范的信号量,无疑是很有成就感的。但时间暂时不允许我们这么做,所以先弄一套缩水版的类 POSIX 信号量,它的函数原型和标准并不完全相同,而且只包含如下系统调用:
sem_t *sem_open(const char *name, unsigned int value);
int sem_wait(sem_t *sem);
int sem_post(sem_t *sem);
int sem_unlink(const char *name);
sem_open() 的功能是创建一个信号量,或打开一个已经存在的信号量。
sem_t 是信号量类型,根据实现的需要自定义。
name 是信号量的名字。不同的进程可以通过提供同样的 name 而共享同一个信号量。如果该信号量不存在,就创建新的名为 name 的信号量;如果存在,就打开已经存在的名为 name 的信号量。
value 是信号量的初值,仅当新建信号量时,此参数才有效,其余情况下它被忽略。当成功时,返回值是该信号量的唯一标识(比如,在内核的地址、ID 等),由另两个系统调用使用。如失败,返回值是 NULL。
sem_wait() 就是信号量的 P 原子操作。如果继续运行的条件不满足,则令调用进程等待在信号量 sem 上。返回 0 表示成功,返回 -1 表示失败。
sem_post() 就是信号量的 V 原子操作。如果有等待 sem 的进程,它会唤醒其中的一个。返回 0 表示成功,返回 -1 表示失败。
sem_unlink() 的功能是删除名为 name 的信号量。返回 0 表示成功,返回 -1 表示失败。
在 kernel 目录下新建 sem.c 文件实现如上功能。然后将 pc.c 从 Ubuntu 移植到 0.11 下,测试自己实现的信号量。inode的锁保护
前面说到,inode的设计使得有多个指针同时指向一个inode成为了可能。因而,修改使用inode的时候就要对其进行独占访问。使用
-ialloc
获取和用ifree
释放的inode必须被保护在ilock
和iunlock
区域中。ilock
-
ilock
既可以实现对inode的独占访问,同时也可以给未初始化的inode进行初始化工作。---
iget
返回的struct inode
可能没有任何有用的内容。为了确保它保存磁盘inode的副本,代码必须调用ilock
。这将锁定inode(以便没有其他进程可以对其进行ilock
),并从磁盘读取尚未读取的inode。+
// Lock the given inode and reads the inode from disk if necessary.
void
ilock(struct inode *ip)
{
struct buf *bp;
struct dinode *dip;
if(ip == 0 || ip->ref < 1)
panic("ilock");
acquiresleep(&ip->lock);
if(ip->valid == 0){
// 通过inode索引号和superblock算出扇区号
bp = bread(ip->dev, IBLOCK(ip->inum, sb));
dip = (struct dinode*)bp->data + ip->inum%IPB;
// 填充ip
ip->type = dip->type;
ip->major = dip->major;
ip->minor = dip->minor;
ip->nlink = dip->nlink;
ip->size = dip->size;
memmove(ip->addrs, dip->addrs, sizeof(ip->addrs));
brelse(bp);
ip->valid = 1;
if(ip->type == 0)
panic("ilock: no type");
}
}由于不小心写完的实验代码被销毁了,因此差不多参考的是这篇文章【戳这里】,修改了一些地方,构成了我的回忆版代码。
+要点1 系统调用修改
详见文章,写得很清楚。
+要点2 sem.c文件的编写
sem_t定义
+-
/* 定义的信号量数据结构: */
typedef struct semaphore_t
{
char name[20];/* 信号量的名称 */
int value; /* 信号量的值 */
int active;//我自己加的,是对象池思想,感觉写得还挺好的2333
struct tast_struct *queue;/* 指向阻塞队列的指针 */
} sem_t;iunlock
---
iunlock
释放inode上的锁。将inode指针的获取与锁定分离有助于在某些情况下避免死锁,例如在目录查找期间。多个进程可以持有指向
-iget
返回的inode的C指针,但一次只能有一个进程锁定inode。+
// Unlock the given inode.
void
iunlock(struct inode *ip)
{
if(ip == 0 || !holdingsleep(&ip->lock) || ip->ref < 1)
panic("iunlock");
releasesleep(&ip->lock);
}-
//信号量表
sem_t sem_list[SEM_LIST_LENGTH];
int str_cmp(const char* s1,const char* s2){
char* p=s1;
int i,len1=0,len2=0;
while(*p!='\0'){
p++;
len1++;
}
p=s2;
while(*p!='\0'){
p++;
len2++;
}
if(len1!=len2) return 1;
for(i=0;i<len1;i++){
if(s1[i]!=s2[i]) return 1;
}
return 0;
}Code: inode content
Overview
--主要讲的是inode本身存储数据的结构
---磁盘上的inode结构体
-struct dinode
包含一个size
和一个块号数组(见图8.3),数组内罗列着存储着该inode数据的块号。前面的
-NDIRECT
个数据块被列在数组中的前NDIRECT
个元素中;这些块称为直接块(direct blocks)。接下来的NINDIRECT
个数据块不在inode中列出,而是在称为间接块(indirect block)的数据块中列出。addrs
数组中的最后一个元素给出了间接块的地址。因此,可以从inode中列出的块加载文件的前12 kB(
-NDIRECT x BSIZE
)字节,而只有在查阅间接块后才能加载下一个256 kB(NINDIRECT x BSIZE
)字节。+
// On-disk inode structure
struct dinode {
// ...
uint addrs[NDIRECT+1]; // Data block addresses
};sem_open
+-
/*
sem_open()的功能是创建一个信号量,或打开一个已经存在的信号量。
*/
sem_t *sys_sem_open(const char * name,unsigned int value)
{
if (name == NULL)
{
errno = 1;
return NULL;
}
/* 首先将信号量的名称赋值到新建的缓冲区中 */
char nbuf[20];
int i;
for(i = 0; i< 20; i++)
{
nbuf[i] = get_fs_byte(name+i);
}
/* 然后开始遍历已有的信号量数组,如果有该名字的信号量,直接返回信号量的地址 */
for(i = 0; i < SEM_LIST_LENGTH; i++)
{
if(sem_list[i].active==1&&!str_cmp(sem_list[i].name, nbuf))
{
return &sem_list[i];
}
}
/* 如果找不到信号量,就开始新建一个名字为name的信号量 */
for(i = 0; i < SEM_LIST_LENGTH; i++)
{
if(sem_list[i].active==0)
{
strcpy(sem_list[i].name, nbuf);
sem_list[i].value = value;
sem_list[i].active=1;
sem_list[i].queue = NULL;
return &sem_list[i];
}
}
//表已满
errno = 1;
return NULL;
}-
bmap
--函数
-bmap
负责封装这个寻找数据块的过程,以便实现我们将很快看到的如readi
和writei
这样的更高级例程。-
bmap(struct inode *ip, uint bn)
返回inodeip
的第bn
个数据块的磁盘块号。如果ip
还没有这样的块,bmap
会分配一个。-
Bmap
使readi
和writei
很容易获取inode的数据。+
// Inode content
//
// The content (data) associated with each inode is stored
// in blocks on the disk. The first NDIRECT block numbers
// are listed in ip->addrs[]. The next NINDIRECT blocks are
// listed in block ip->addrs[NDIRECT].
// Return the disk block address of the nth block in inode ip.
// If there is no such block, bmap allocates one.
static uint
bmap(struct inode *ip, uint bn)
{
uint addr, *a;
struct buf *bp;
// 如果为direct block
if(bn < NDIRECT){
if((addr = ip->addrs[bn]) == 0)
ip->addrs[bn] = addr = balloc(ip->dev);
return addr;
}
bn -= NDIRECT;
// 如果为indirect block
if(bn < NINDIRECT){
// Load indirect block, allocating if necessary.
if((addr = ip->addrs[NDIRECT]) == 0)
ip->addrs[NDIRECT] = addr = balloc(ip->dev);
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
if((addr = a[bn]) == 0){
// 如果没有,会分配一个
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
panic("bmap: out of range");
}sem_wait
+-
/*
sem_wait()就是信号量的P原子操作。
如果继续运行的条件不满足,则令调用进程等待在信号量sem上。
返回0表示成功,返回-1表示失败。
*/
int sys_sem_wait(sem_t * sem)
{
/* 判断:如果传入的信号量是无效信号量,P操作失败,返回-1 */
if(sem == NULL || sem < sem_list || sem > sem_list + SEM_LIST_LENGTH)
{
errno=1;
return -1;
}
/* 关中断 */
cli();
while(sem->value < 0)
{
sleep_on(&(sem->queue));
}
sem->value--;
/* 开中断 */
sti();
return 0;
}itrunc
---
itrunc
释放文件的块,将inode的size
重置为零。-
Itrunc
首先释放直接块,然后释放间接块中列出的块,最后释放间接块本身。readi
---
readi
和writei
都是从检查ip->type == T_DEV
开始的。这种情况处理的是数据不在文件系统中的特殊设备;我们将在文件描述符层返回到这种情况。+
// Read data from inode.数据大小为n,从off开始,读到dst处
// Caller must hold ip->lock.
// If user_dst==1, then dst is a user virtual address;
// otherwise, dst is a kernel address.
int
readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n)
{
uint tot, m;
struct buf *bp;
if(off > ip->size || off + n < off)
return 0;
if(off + n > ip->size)
n = ip->size - off;
// 主循环处理文件的每个块,将数据从缓冲区复制到dst
for(tot=0; tot<n; tot+=m, off+=m, dst+=m){
bp = bread(ip->dev, bmap(ip, off/BSIZE));
m = min(n - tot, BSIZE - off%BSIZE);
if(either_copyout(user_dst, dst, bp->data + (off % BSIZE), m) == -1) {
brelse(bp);
tot = -1;
break;
}
brelse(bp);
}
return tot;
}sem_post
+-
/*
sem_post()就是信号量的V原子操作。
如果有等待sem的进程,它会唤醒其中的一个。
返回0表示成功,返回-1表示失败。
*/
int sys_sem_post(sem_t * sem)
{
/* 判断:如果传入的信号量是无效信号量,V操作失败,返回-1 */
if(sem == NULL || sem < sem_list || sem > sem_list + SEM_LIST_LENGTH)
{
return -1;
}
/* 关中断 */
cli();
sem->value++;
/* 如果有等待sem的进程,它会唤醒其中的一个。 */
if(sem->value <= 0)
{
wake_up(&(sem->queue));
}
/* 开中断 */
sti();
return 0;
}writei
+
// Write data to inode.
// Caller must hold ip->lock.
// If user_src==1, then src is a user virtual address;
// otherwise, src is a kernel address.
int
writei(struct inode *ip, int user_src, uint64 src, uint off, uint n)
{
uint tot, m;
struct buf *bp;
if(off > ip->size || off + n < off)
return -1;
// writei会自动增长文件,除非达到文件的最大大小
if(off + n > MAXFILE*BSIZE)
return -1;
for(tot=0; tot<n; tot+=m, off+=m, src+=m){
bp = bread(ip->dev, bmap(ip, off/BSIZE));
m = min(n - tot, BSIZE - off%BSIZE);
if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
brelse(bp);
n = -1;
break;
}
log_write(bp);
brelse(bp);
}
if(n > 0){
if(off > ip->size)
// 说明扩大了文件大小,需要修改
ip->size = off;
// write the i-node back to disk even if the size didn't change
// because the loop above might have called bmap() and added a new
// block to ip->addrs[].
iupdate(ip);
}
return n;
}sem_unlink
+-
/*
sem_unlink()的功能是删除名为name的信号量。
返回0表示成功,返回-1表示失败。
*/
int sys_sem_unlink(const char *name)
{
if (name == NULL){
errno = 1;
return -1;
}
/* 首先将信号量的名称赋值到新建的缓冲区中 */
char nbuf[20];
int i;
for (i = 0; i < 20; i++)
{
nbuf[i] = get_fs_byte(name + i);
if (nbuf[i] == '\0')
break;
}
for (i = 0; i < SEM_LIST_LENGTH; i++)
{
if (str_cmp(sem_list[i].name, nbuf)==0)
{
sem_list[i].active=0;
return 0;
}
}
return -1;
}stati
--函数
-stati
将inode元数据复制到stat
结构体中,该结构通过stat
系统调用向用户程序公开。在
-defs.h
中可看到inode结构体是private的,而stat是public的。Directory layer
数据结构
--目录的内部实现很像文件。其inode的
-type
为T_DIR
,其数据是directory entries的集合。每个entry都是一个
-struct dirent
。也就是说这一层其实本质上是一个大小一定的map,该map自身也存放在inode中,大小为inode的大小,每个表项entry映射了目录名和文件inode。所以接下来介绍的函数我们完全可以从hashmap增删改查的角度去理解。
--
// Directory is a file containing a sequence of dirent structures.
struct dirent {
ushort inum;// 如果为0,说明该entry free
char name[DIRSIZ];
};-
相关函数
dirlookup
--函数
-dirlookup
在directory中搜索具有给定名称的entry。它返回的指向enrty.inum相应的inode是非独占的【通过iget获取】,也即无锁状态。它还会把
-*poff
设置为所需的entry的字节偏移量。为什么要返回未锁定的inode?是因为调用者已锁定
-dp
,因此,如果对.
进行查找,则在返回之前尝试锁定inode将导致重新锁定dp
并产生死锁【确实】(还有更复杂的死锁场景,涉及多个进程和..
,父目录的别名。.
不是唯一的问题。)所以锁定交给caller来做。caller可以解锁
-dp
,然后锁定该函数返回的ip
,确保它一次只持有一个锁。-
// Look for a directory entry in a directory.
// If found, set *poff to byte offset of entry.
struct inode*
dirlookup(struct inode *dp, char *name, uint *poff)
{
uint off, inum;
struct dirent de;
if(dp->type != T_DIR)
panic("dirlookup not DIR");
// new level of abstraction,可以把directory的inode看作一个表文件,每个表项都是一个entry
for(off = 0; off < dp->size; off += sizeof(de)){
// 从directory中获取entry,也即从inode中获取数据
if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlookup read");
// free
if(de.inum == 0)
continue;
if(namecmp(name, de.name) == 0){
// entry matches path element
if(poff)
*poff = off;
inum = de.inum;
return iget(dp->dev, inum);
}
}
return 0;
}dirlink
+
// Write a new directory entry (name, inum) into the directory dp.
int
dirlink(struct inode *dp, char *name, uint inum)
{
int off;
struct dirent de;
struct inode *ip;
// Check that name is not present.
if((ip = dirlookup(dp, name, 0)) != 0){
iput(ip);
return -1;
}
// Look for an empty dirent.
for(off = 0; off < dp->size; off += sizeof(de)){
if(readi(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlink read");
if(de.inum == 0)
break;
}
// 如果没找到空闲的则调用writei自动增长inode,添加新表项
strncpy(de.name, name, DIRSIZ);
de.inum = inum;
if(writei(dp, 0, (uint64)&de, off, sizeof(de)) != sizeof(de))
panic("dirlink");
return 0;
}pc.c
+-
_syscall2(sem_t *, sem_open, const char *, name, unsigned int, value);
_syscall1(int, sem_wait, sem_t *, sem);
_syscall1(int, sem_post, sem_t *, sem);
_syscall1(int, sem_unlink, const char *, name);
const char* filename="buffer.txt";
sem_t* empty;
sem_t* full;
sem_t* mutex;
int fd;
void Producer(){
int i;
int tmp;
char s[5]={0};
for(i=0;i<=500;i++){
sem_wait(empty);
sem_wait(mutex);
tmp=lseek(fd,0,SEEK_CUR);
lseek(fd,0,SEEK_END);
sprintf(s,"%03d\n",i);
write(fd,s,4);
lseek(fd,tmp,SEEK_SET);
sem_post(mutex);
sem_post(full);
}
}
void Consumer(){
char s[5]={0};
sem_wait(full);
sem_wait(mutex);
read(fd,s,4);
printf("%d : %s",getpid(),s);
sem_post(mutex);
sem_post(empty);
}
int main(){
int i;
fd=open(filename,O_RDWR|O_CREAT);
printf("%d\n",errno);
empty=sem_open("empty",10);
full=sem_open("full",0);
mutex=sem_open("mutex",1);
if(!fork()){
Producer();
return 0;
}
for(i=0;i<10;i++){
if(!fork()){
int c=50;
while(c--) Consumer();
}
}
close(fd);
sem_unlink("empty");
sem_unlink("full");
sem_unlink("mutex");
return 0;
}Pathname layer
--Path name lookup involves a succession of calls to dirlookup, one for each path component.
-namei和nameiparent
--Namei (kernel/fs.c:661) evaluates path and returns the corresponding inode.
-函数
-nameiparent
是一个变体:它在最后一个元素之前停止,返回父目录的inode并将最后一个元素复制到name
中。两者都调用通用函数namex
来完成实际工作。-
struct inode*
namei(char *path)
{
char name[DIRSIZ];
return namex(path, 0, name);
}-
struct inode*
nameiparent(char *path, char *name)
{
return namex(path, 1, name);
}namex
--
Namex
首先决定路径解析的开始位置。如果路径以“ / ”开始,则从根目录开始解析;否则,从当前目录开始。
-然后,它使用
-skipelem
依次考察路径的每个元素。循环的每次迭代都必须在当前索引结点ip
中查找name
。迭代首先给
-ip
上锁并检查它是否是一个目录。如果不是,则查找失败。如果caller是
-nameiparent
,并且这是最后一个路径元素,则根据nameiparent
的定义,循环会提前停止;最后一个路径元素已经复制到name
中【在上一轮循坏中做了这件事】,因此namex
只需返回解锁的ip
。最后,循环将使用
-dirlookup
查找路径元素,并通过设置ip = next
为下一次迭代做准备。当循环用完路径元素时,它返回ip
。注:
--
+- 在每次迭代中锁定
-ip
是必要的,不是因为ip->type
可以被更改,而是因为在ilock
运行之前,ip->type
不能保证已从磁盘加载,所以得用到ilock保证一定会被加载的这个性质。这部分踩过的坑:
++
+- +
在用户态和核心态之间传递参数【这个我没考虑到】
++ +
指针参数传递的是应用程序所在地址空间的逻辑地址,
在内核中如果直接访问这个地址,访问到的是内核空间中的数据,不会是用户空间的。
所以这里还需要一点儿特殊工作,才能在内核中从用户空间得到数据。这段代码就是在做这个。
+
/* 首先将信号量的名称赋值到新建的缓冲区中 */
char nbuf[20];
int i = 0;
for(; i< 20; i++)
{
nbuf[i] = get_fs_byte(name+i);
}- +
这一段代码值得学习
+
- +
一个第一眼看傻掉了的问题
++ +
//sleep函数的签名
void sleep_on(struct task_struct **p);
//一开始初始化队列为空
sem_list[i].queue = NULL;
//使用sleep
sleep_on(&(sem->queue));如果队列为空的时候,传入sleep_on的是不是NULL呢?
+其实这个本质上是type* p=NULL,&p是不是NULL的问题。虽然知道不是,但还是写个程序测试一下:
+
typedef struct {
int value;
}haha;
void isNULL(haha** a){
printf("%d",a==NULL);
}
int main(){
haha *h=NULL;
isNULL(&h);
return 0;
}
//result:0- +
sem_post签名与实现矛盾
++ +
wake_up() 的功能是唤醒链表上睡眠的所有进程。
sem_post() 就是信号量的 V 原子操作。如果有等待 sem 的进程,它会唤醒其中的一个。以上都是指导书的内容。这个“所有”和“一个”的用意我不大明白。也许唤醒所有进程,其中一个抢到了锁,其他的全睡了,这个也被认为是唤醒其中一个吧()
+- +
聪明的越界处理【未考虑到】
++ +
/* 判断:如果传入的信号量是无效信号量,V操作失败,返回-1 */
if(sem == NULL || sem < sem_list || sem > sem_list + SEM_LIST_LENGTH)
{
return -1;
}毕竟有效的信号量都是引用的信号量表的信号量。所以地址越界的自然无效。
+- +
最坑的一点
+其实指导书提醒了
++-下面描述的问题未必具有普遍意义,仅做为提醒,请实验者注意。
+include/string.h 实现了全套的 C 语言字符串操作,而且都是采用汇编 + inline 方式优化。
+但在使用中,某些情况下可能会遇到一些奇怪的问题。比如某人就遇到
strcmp()
会破坏参数内容的问题。如果调试中遇到有些 “诡异” 的情况,可以试试不包含头文件,一般都能解决。不包含string.h
,就不会用 inline 方式调用这些函数,它们工作起来就趋于正常了。+
// Look up and return the inode for a path name.
// If parent != 0, return the inode for the parent and copy the final
// path element into name, which must have room for DIRSIZ bytes.
// Must be called inside a transaction since it calls iput().
static struct inode*
namex(char *path, int nameiparent, char *name)
{
struct inode *ip, *next;
if(*path == '/')
ip = iget(ROOTDEV, ROOTINO);
else
ip = idup(myproc()->cwd);
// 使用skipelem依次考察路径的每个元素
while((path = skipelem(path, name)) != 0){
ilock(ip);
if(ip->type != T_DIR){
iunlockput(ip);
return 0;
}
if(nameiparent && *path == '\0'){
// Stop one level early.
iunlock(ip);
return ip;
}
if((next = dirlookup(ip, name, 0)) == 0){
iunlockput(ip);
return 0;
}
iunlockput(ip);
ip = next;
}
if(nameiparent){
iput(ip);
return 0;
}
return ip;
}但是具体表现跟它说的差距有点大()
+我是全部检查没问题了,然后上linux0.11真机运行。PID:number这样的信息全部打印出来了,没啥问题,但是打印完操作系统就会寄,大多数极端情况就直接重启了,小部分还会温和地提醒以下报错信息然后死循环
++
kernel panic: trying to free up swapper memory space
in swapper task - not syncing最后尝试着修改去掉string.h,才得到了正确的结果,泪目。
+proc文件系统
+参考文章:
+操作系统实验08-proc文件系统的实现
---
namex
过程可能需要很长时间才能完成:它可能涉及多个磁盘操作来读取路径名中所遍历目录的索引节点和目录块(如果它们不在buffer cache中)。Xv6 is carefully designed,如果一个内核线程对
-namex
的调用在磁盘I/O上阻塞,另一个查找不同路径名的内核线程可以同时进行。Namex
locks each directory in the path separately so that lookups in different directories can proceed in parallel.锁细粒度化This concurrency introduces some challenges. For example, while one kernel thread is looking up a pathname another kernel thread may be changing the directory tree by unlinking a directory. A potential risk is that a lookup may be searching a directory that has been deleted by another kernel thread and its blocks have been re-used for another directory or file.一个潜在的风险是,查找可能正在搜索已被另一个内核线程删除且其块已被重新用于另一个目录或文件的目录。
-Xv6避免了这种竞争,也就是说,你查到的inode保证暂时不会被释放,里面的内容还是真的,而不会被重新利用从而导致里面的内容变样。
-例如,在
-namex
中执行dirlookup
时,lookup线程持有目录上的锁,dirlookup
返回使用iget
获得的inode。Iget
增加索引节点的引用计数。只有在从dirlookup
接收inode之后,namex
才会释放目录上的锁。现在,另一个线程可以从目录中取消inode的链接,但是xv6还不会删除inode,因为inode的引用计数仍然大于零。另一个风险是死锁。例如,查找“
+.
”时,next
指向与ip
相同的inode【确实】。在释放ip
上的锁之前锁定next
将导致死锁【为什么???难道不是会由于在acquire时已经持有锁,从而爆panic("acquire")
吗?】。为了避免这种死锁,namex
在获得下一个目录的锁之前解锁该目录。这里我们再次看到为什么iget
和ilock
之间的分离很重要。在 Linux 0.11 上实现 procfs(proc 文件系统)内的 psinfo 结点。当读取此结点的内容时,可得到系统当前所有进程的状态信息。例如,用 cat 命令显示
+/proc/psinfo
和/proc/hdinfo
的内容,可得到:+ +
$ cat /proc/psinfo
pid state father counter start_time
0 1 -1 0 0
1 1 0 28 1
4 1 1 1 73
3 1 1 27 63
6 0 4 12 817
$ cat /proc/hdinfo
total_blocks: 62000;
free_blocks: 39037;
used_blocks: 22963;
...+
procfs
及其结点要在内核启动时自动创建。相关功能实现在
fs/proc.c
文件内。File descriptor layer
-Unix的一个很酷的方面是,Unix中的大多数资源都表示为文件,包括控制台、管道等设备,当然还有真实文件。文件描述符层是实现这种一致性的层。
+必备知识
要点1 procfs简介
+-正式的 Linux 内核实现了
+procfs
,它是一个**虚拟文件系统**,通常被 mount(挂载) 到/proc
目录上,通过虚拟文件和虚拟目录的方式提供访问系统参数的机会,所以有人称它为 “了解系统信息的一个窗口”。这些虚拟的文件和目录**并没有真实地存在在磁盘**上,而是内核中各种数据的一种直观表示。虽然是虚拟的,但它们都可以通过标准的系统调用(
+open()
、read()
等)访问。其实,Linux 的很多系统命令就是通过读取
/proc
实现的。例如uname -a
的部分信息就来自/proc/version
,而uptime
的部分信息来自/proc/uptime
和/proc/loadavg
。数据结构
-Xv6为每个进程提供了自己的打开文件表或文件描述符。每个打开的文件都由一个
-struct file
表示,它是inode或管道的封装,加上一个I/O偏移量。每次调用
-open
都会创建一个新的打开文件(一个新的struct file
):如果多个进程独立地打开同一个文件,那么不同的实例将具有不同的I/O偏移量。另一方面,单个打开的文件(同一个
+struct file
)可以多次出现在一个进程的文件表中,也可以出现在多个进程的文件表中。如果一个进程使用open
打开文件,然后使用dup
创建别名,或使用fork
与子进程共享,就会发生这种情况。要点2 基本思路
+-Linux 是通过文件系统接口实现
+procfs
,并在启动时自动将其 mount 到/proc
目录上。此目录下的所有内容都是随着系统的运行自动建立、删除和更新的,而且它们完全存在于内存中,不占用任何外存空间。
+Linux 0.11 还没有实现虚拟文件系统,也就是,还没有提供增加新文件系统支持的接口。所以本实验只能在现有文件系统的基础上,通过打补丁的方式模拟一个
+procfs
。Linux 0.11 使用的是 Minix 的文件系统,这是一个典型的基于
inode
的文件系统,《注释》一书对它有详细描述。它的每个文件都要对应至少一个 inode,而 inode 中记录着文件的各种属性,包括文件类型。文件类型有普通文件、目录、字符设备文件和块设备文件等。在内核中,每种类型的文件都有不同的处理函数与之对应。我们可以增加一种新的文件类型——proc 文件,并在相应的处理函数内实现 procfs 要实现的功能。+
struct file {
enum { FD_NONE, FD_PIPE, FD_INODE, FD_DEVICE } type;
int ref; // reference count
char readable;
char writable;
struct pipe *pipe; // FD_PIPE
struct inode *ip; // FD_INODE and FD_DEVICE
uint off; // FD_INODE
short major; // FD_DEVICE
};步骤
要点1 新增proc文件类型
include/sys/stat.h 新增:
+-
ftable
-所有在系统中打开的文件都会被放入global file table
-ftable
中。-
ftable
具有分配文件(filealloc
)、创建重复引用(filedup
)、释放引用(fileclose
)以及读取和写入数据(fileread
和filewrite
)的函数。前三个都很常规,跟之前的xxalloc、xxfree的思路是一样的。
-函数
+filestat
、fileread
和filewrite
实现对文件的stat
、read
和write
操作。要点2 修改mknod()函数和init()函数
+-psinfo 结点要通过
mknod()
系统调用建立,所以要让它支持新的文件类型。filealloc
+
// Allocate a file structure.
struct file*
filealloc(void)
{
struct file *f;
acquire(&ftable.lock);
for(f = ftable.file; f < ftable.file + NFILE; f++){
if(f->ref == 0){
f->ref = 1;
release(&ftable.lock);
return f;
}
}
release(&ftable.lock);
return 0;
}直接修改
+fs/namei.c
文件中的sys_mknod()
函数中的一行代码,如下:-
if (S_ISBLK(mode) || S_ISCHR(mode) || S_ISPROC(mode))
inode->i_zone[0] = dev;
// 文件系统初始化filedup
+
// Increment ref count for file f.
struct file*
filedup(struct file *f)
{
acquire(&ftable.lock);
if(f->ref < 1)
panic("filedup");
f->ref++;
release(&ftable.lock);
return f;
}+-内核初始化的全部工作是在
+main()
中完成,而main()
在最后从内核态切换到用户态,并调用init()
。+
init()
做的第一件事情就是挂载根文件系统:-
void init(void) {
// ……
setup((void *) &drive_info);
// ……
}fileclose
+
// Close file f. (Decrement ref count, close when reaches 0.)
void
fileclose(struct file *f)
{
struct file ff;
acquire(&ftable.lock);
if(f->ref < 1)
panic("fileclose");
if(--f->ref > 0){
release(&ftable.lock);
return;
}
ff = *f;
f->ref = 0;
f->type = FD_NONE;
release(&ftable.lock);
if(ff.type == FD_PIPE){
pipeclose(ff.pipe, ff.writable);
} else if(ff.type == FD_INODE || ff.type == FD_DEVICE){
begin_op();
iput(ff.ip);
end_op();
}
}+
procfs
的初始化工作**应该在根文件系统挂载之后开始**。它包括两个步骤:+
- +
(1)建立
+/proc
目录;建立/proc
目录下的各个结点。本实验只建立/proc/psinfo
。- +
(2)建立目录和结点分别需要调用
+mkdir()
和mknod()
系统调用。因为初始化时已经在用户态,所以不能直接调用sys_mkdir()
和sys_mknod()
。必须在初始化代码所在文件中实现这两个系统调用的用户态接口。-
_syscall2(int,mkdir,const char*,name,mode_t,mode);
_syscall3(int,mknod,const char*,filename,mode_t,mode,dev_t,dev);filestat
-Filestat只允许在inode上操作并且调用了
+stati
+
mkdir()
时 mode 参数的值可以是 “0755”(对应rwxr-xr-x
),表示只允许 root 用户改写此目录,其它人只能进入和读取此目录。procfs 是一个只读文件系统,所以用
+mknod()
建立 psinfo 结点时,必须通过 mode 参数将其设为只读。建议使用S_IFPROC|0444
做为 mode 值,表示这是一个 proc 文件,权限为 0444(r–r–r–),对所有用户只读。+
mknod()
的第三个参数 dev 用来说明结点所代表的设备编号。对于 procfs 来说,此编号可以完全自定义。proc 文件的处理函数将通过这个编号决定对应文件包含的信息是什么。例如,可以把 0 对应 psinfo,1 对应 meminfo,2 对应 cpuinfo。+
// Get metadata about file f.
// addr is a user virtual address, pointing to a struct stat.
int
filestat(struct file *f, uint64 addr)
{
struct proc *p = myproc();
struct stat st;
// 仅允许文件/设备执行
if(f->type == FD_INODE || f->type == FD_DEVICE){
ilock(f->ip);
stati(f->ip, &st);
iunlock(f->ip);
if(copyout(p->pagetable, addr, (char *)&st, sizeof(st)) < 0)
return -1;
return 0;
}
return -1;
}也就是说,打开linux-0.11/init/main.c
+加入:
+-
_syscall2(int,mkdir,const char*,name,mode_t,mode);
_syscall3(int,mknod,const char*,filename,mode_t,mode,dev_t,dev);fileread
+
// Read from file f.
// addr is a user virtual address.
int
fileread(struct file *f, uint64 addr, int n)
{
int r = 0;
// 首先检查是否可读
if(f->readable == 0)
return -1;
if(f->type == FD_PIPE){
r = piperead(f->pipe, addr, n);
} else if(f->type == FD_DEVICE){
if(f->major < 0 || f->major >= NDEV || !devsw[f->major].read)
return -1;
r = devsw[f->major].read(1, addr, n);
} else if(f->type == FD_INODE){
ilock(f->ip);
if((r = readi(f->ip, 1, addr, f->off, n)) > 0)
// 移动文件指针偏移量
f->off += r;
iunlock(f->ip);
} else {
panic("fileread");
}
return r;
}在init函数中,添加:
+-
mkdir("/proc",0755);
mknod("/proc/psinfo",S_IFPROC|0400,0);
//其余文件以此类推...Code: System calls
--通过使用底层提供的函数,大多数系统调用的实现都很简单(请参阅***kernel/sysfile.c***)。有几个调用值得仔细看看。
-以下介绍的函数都在
-kernel/sysfile.c
中。sys_link
这个函数的功能是给文件old加上一个链接,这个链接存在于文件new的父目录。感觉也就相当于把文件从old复制到new处了。具体实现逻辑就是要给该文件所在目录添加一个entry,name=新名字,inode=该文件的inode。
-+
// Create the path new as a link to the same inode as old.
uint64
sys_link(void)
{
char name[DIRSIZ], new[MAXPATH], old[MAXPATH];
struct inode *dp, *ip;
if(argstr(0, old, MAXPATH) < 0 || argstr(1, new, MAXPATH) < 0)
return -1;
// 首先先增加nlink
begin_op();
// 通过path找到ip结点
if((ip = namei(old)) == 0){
end_op();
return -1;
}
ilock(ip);
// directory不能被link
if(ip->type == T_DIR){
iunlockput(ip);
end_op();
return -1;
}
ip->nlink++;
// 修改一次字段就需要update一次
iupdate(ip);
iunlock(ip);
// 然后再在目录中登记新的entry
// 找到new的parent,也即new所在目录
if((dp = nameiparent(new, name)) == 0)
goto bad;
ilock(dp);
// 在目录中添加一个entry,名字为给定的新名字,inode依旧为原来的inode
// new的父目录必须存在并且与现有inode位于同一设备上
if(dp->dev != ip->dev || dirlink(dp, name, ip->inum) < 0){
iunlockput(dp);
goto bad;
}
iunlockput(dp);
iput(ip);
end_op();
return 0;
bad:
ilock(ip);
ip->nlink--;
iupdate(ip);
iunlockput(ip);
end_op();
return -1;
}编译运行即可看到:
+-
create
-它是三个文件创建系统调用的泛化:带有
+O_CREATE
标志的open
生成一个新的普通文件,mkdir
生成一个新目录,mkdev
生成一个新的设备文件。+-这些信息至少说明,psinfo 被正确
open()
了。所以我们不需要对sys_open()
动任何手脚,唯一要打补丁的,是sys_read()
。创建一个新的inode结点,结点名包含在
-path
内。返回一个锁定的inode。由于使用了
-iupdate
等,所以该函数只能在事务中被调用。- -
static struct inode*
create(char *path, short type, short major, short minor)
{
struct inode *ip, *dp;
char name[DIRSIZ];
// 获取结点父目录
if((dp = nameiparent(path, name)) == 0)
return 0;
ilock(dp);
if((ip = dirlookup(dp, name, 0)) != 0){
// 说明文件已存在
iunlockput(dp);
ilock(ip);
if(type == T_FILE && (ip->type == T_FILE || ip->type == T_DEVICE))
// 说明此时caller为open(type == T_FILE),open调用create只能是用于创建文件
return ip;
iunlockput(ip);
return 0;
}
if((ip = ialloc(dp->dev, type)) == 0)
panic("create: ialloc");
ilock(ip);
ip->major = major;
ip->minor = minor;
ip->nlink = 1;
iupdate(ip);
if(type == T_DIR){ // Create . and .. entries.
dp->nlink++; // for ".."
iupdate(dp);
// No ip->nlink++ for ".": avoid cyclic ref count.
// 所以其实.和..本质上是link
if(dirlink(ip, ".", ip->inum) < 0 || dirlink(ip, "..", dp->inum) < 0)
panic("create dots");
}
if(dirlink(dp, name, ip->inum) < 0)
panic("create: dirlink");
iunlockput(dp);
return ip;
}sys_mkdir
- -
uint64
sys_mkdir(void)
{
char path[MAXPATH];
struct inode *ip;
begin_op();
if(argstr(0, path, MAXPATH) < 0 || (ip = create(path, T_DIR, 0, 0)) == 0){
end_op();
return -1;
}
iunlockput(ip);
end_op();
return 0;
}sys_open
-+
Sys_open
是最复杂的,因为创建一个新文件只是它能做的一小部分。要点3 修改read(),让proc可读
+-首先分析
+sys_read
(在文件fs/read_write.c
中)要在这里一群 if 的排比中,加上
+S_IFPROC()
的分支,进入对 proc 文件的处理函数。需要传给处理函数的参数包括:+
- +
inode->i_zone[0]
,这就是mknod()
时指定的dev
——设备编号- +
buf
,指向用户空间,就是read()
的第二个参数,用来接收数据- +
count
,就是read()
的第三个参数,说明buf
指向的缓冲区大小- +
&file->f_pos
,f_pos
是上一次读文件结束时“文件位置指针”的指向。这里必须传指针,因为处理函数需要根据传给buf
的数据量修改f_pos
的值。- -
uint64
sys_open(void)
{
char path[MAXPATH];
int fd, omode;
struct file *f;
struct inode *ip;
int n;
if((n = argstr(0, path, MAXPATH)) < 0 || argint(1, &omode) < 0)
return -1;
begin_op();
if(omode & O_CREATE){
ip = create(path, T_FILE, 0, 0);
// 创建失败
if(ip == 0){
end_op();
return -1;
}
} else {
// 文件不存在
if((ip = namei(path)) == 0){
end_op();
return -1;
}
// Create返回一个锁定的inode,但namei不锁定,因此sys_open必须锁定inode本身。
ilock(ip);
// 非文件,为目录并且非只读
// 所以说想要open一个目录的话只能以只读模式打开
if(ip->type == T_DIR && omode != O_RDONLY){
iunlockput(ip);
end_op();
return -1;
}
}
if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
iunlockput(ip);
end_op();
return -1;
}
// 获取file结构体和文件描述符。
if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
if(f)
fileclose(f);
iunlockput(ip);
end_op();
return -1;
}
// 没有其他进程可以访问部分初始化的文件,因为它仅位于当前进程的表中,因而这里可以不用上锁
if(ip->type == T_DEVICE){
f->type = FD_DEVICE;
f->major = ip->major;
} else {
f->type = FD_INODE;
f->off = 0;
}
f->ip = ip;
f->readable = !(omode & O_WRONLY);
f->writable = (omode & O_WRONLY) || (omode & O_RDWR);
// 如果使用了这个标志,调用 open 函数打开文件的时候会将文件原本的内容全部丢弃,文件大小变为 0。
if((omode & O_TRUNC) && ip->type == T_FILE){
itrunc(ip);
}
iunlock(ip);
end_op();
return fd;
}sys_pipe
+
uint64
sys_pipe(void)
{
uint64 fdarray; // user pointer to array of two integers用来接收pipe两端的文件描述符
struct file *rf, *wf;
int fd0, fd1;
struct proc *p = myproc();
if(argaddr(0, &fdarray) < 0)
return -1;
if(pipealloc(&rf, &wf) < 0)
return -1;
fd0 = -1;
if((fd0 = fdalloc(rf)) < 0 || (fd1 = fdalloc(wf)) < 0){
if(fd0 >= 0)
p->ofile[fd0] = 0;
fileclose(rf);
fileclose(wf);
return -1;
}
if(copyout(p->pagetable, fdarray, (char*)&fd0, sizeof(fd0)) < 0 ||
copyout(p->pagetable, fdarray+sizeof(fd0), (char *)&fd1, sizeof(fd1)) < 0){
p->ofile[fd0] = 0;
p->ofile[fd1] = 0;
fileclose(rf);
fileclose(wf);
return -1;
}
return 0;
}依照指导书,在read_write.c添加如下语句:
+-
extern int proc_handler(unsigned short dev,char* buf,int count,off_t* f_pos);
int sys_read(...){
// ...
if(S_ISPROC(inode->i_mode)){
return proc_handler(inode->i_zone[0],buf,count,&file->f_pos);
}
// ...
}Real world
--实际操作系统中的buffer cache比xv6复杂得多,但它有两个相同的用途:缓存和同步对磁盘的访问。
-与UNIX V6一样,Xv6的buffer cache使用简单的最近最少使用(LRU)替换策略;有许多更复杂的策略可以实现,每种策略都适用于某些工作场景,而不适用于其他工作场景。更高效的LRU缓存将消除链表,而改为使用哈希表进行查找,并使用堆进行LRU替换【跟我们在lock中实现的一样,再多个堆优化】。现代buffer cache通常与虚拟内存系统集成,以支持内存映射文件。
-Xv6的日志系统效率低下。提交不能与文件系统调用同时发生。系统记录整个块,即使一个块中只有几个字节被更改。它执行同步日志写入,每次写入一个块,每个块可能需要整个磁盘旋转时间。真正的日志系统解决了所有这些问题。
-文件系统布局中最低效的部分是目录,它要求在每次查找期间对所有磁盘块进行线性扫描【确实】。当目录只有几个磁盘块时,这是合理的,但对于包含许多文件的目录来说,开销巨大。Microsoft Windows的NTFS、Mac OS X的HFS和Solaris的ZFS(仅举几例)将目录实现为磁盘上块的平衡树。这很复杂,但可以保证目录查找在对数时间内完成(即时间复杂度为O(logn))。
-Xv6对于磁盘故障的解决很初级:如果磁盘操作失败,Xv6就会调用
-panic
。这是否合理取决于硬件:如果操作系统位于使用冗余屏蔽磁盘故障的特殊硬件之上,那么操作系统可能很少看到故障,因此panic
是可以的。另一方面,使用普通磁盘的操作系统应该预料到会出现故障,并能更优雅地处理它们,这样一个文件中的块丢失不会影响文件系统其余部分的使用。Xv6要求文件系统安装在单个磁盘设备上,且大小不变。随着大型数据库和多媒体文件对存储的要求越来越高,操作系统正在开发各种方法来消除“每个文件系统一个磁盘”的瓶颈。基本方法是将多个物理磁盘组合成一个逻辑磁盘。RAID等硬件解决方案仍然是最流行的,但当前的趋势是在软件中尽可能多地实现这种逻辑。这些软件实现通常允许通过动态添加或删除磁盘来扩展或缩小逻辑设备等丰富功能。当然,一个能够动态增长或收缩的存储层需要一个能够做到这一点的文件系统:xv6使用的固定大小的inode块阵列在这样的环境中无法正常工作。将磁盘管理与文件系统分离可能是最干净的设计,但两者之间复杂的接口导致了一些系统(如Sun的ZFS)将它们结合起来。
-Xv6的文件系统缺少现代文件系统的许多其他功能;例如,它缺乏对快照和增量备份的支持。
-现代Unix系统允许使用与磁盘存储相同的系统调用访问多种资源:命名管道、网络连接、远程访问的网络文件系统以及监视和控制接口,如
-/proc
。不同于xv6中fileread
和filewrite
的if
语句,这些系统通常为每个打开的文件提供一个函数指针表【确实有印象】,每个操作一个,并通过函数指针来援引inode的调用实现。网络文件系统和用户级文件系统提供了将这些调用转换为网络RPC并在返回之前等待响应的函数。(注:Linux 内核提供了一种通过
-/proc
文件系统,在运行时访问内核内部数据结构、改变内核设置的机制。proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为访问系统内核数据的操作提供接口。)Lab: file system
--In this lab you will add large files【大文件支持】 and symbolic links【软链接】 to the xv6 file system.
-不过做完这个实验,给我的一种感觉就是磁盘管理和内存管理真的有很多相似之处,不过也许它们所代表的思想也很普遍。
-Large files
实验内容
Overview
-In this assignment you’ll increase the maximum size of an xv6 file.
-Currently xv6 files are limited to 268 blocks, or 268*BSIZE bytes (BSIZE is 1024 in xv6). This limit comes from the fact that an xv6 inode contains 12 “direct” block numbers and one “singly-indirect” block number, which refers to a block that holds up to 256 more block numbers, for a total of 12+256=268 blocks.
-You’ll change the xv6 file system code to support a “doubly-indirect” block in each inode, containing 256 addresses of singly-indirect blocks, each of which can contain up to 256 addresses of data blocks. The result will be that a file will be able to consist of up to 65803 blocks, or 256*256+256+11 blocks (11 instead of 12, because we will sacrifice one of the direct block numbers for the double-indirect block).
+要点4 编写pro文件的处理函数
+-proc 文件的处理函数的功能是根据设备编号,把不同的内容写入到用户空间的 buf。写入的数据要从
+f_pos
指向的位置开始,每次最多写 count 个字节,并根据实际写入的字节数调整f_pos
的值,最后返回实际写入的字节数。当设备编号表明要读的是 psinfo 的内容时,就要按照 psinfo 的形式组织数据。实现此函数可能要用到如下几个函数:
++
+- malloc() 函数
+- free() 函数
+包含
linux/kernel.h
头文件后,就可以使用malloc()
和free()
函数。它们是可以被核心态代码调用的,唯一的限制是一次申请的内存大小不能超过一个页面。Preliminaries
-If at any point during the lab you find yourself having to rebuild the file system from scratch, you can run
+make clean
which forces make to rebuild fs.img.+-进程的信息就来源于内核全局结构数组
+struct task_struct * task[NR_TASKS]
中,具体读取细节可参照sched.c
中的函数schedule()
。可以借鉴一下代码:
+
for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)
if (*p)
(*p)->counter = ((*p)->counter >> 1)+...;What to Look At
意思就是要我们去看一眼fs.h,bmap,以及了解一下逻辑地址bn如何转化为blockno。这个我是知道的。
-Your Job
-Modify
-bmap()
so that it implements a doubly-indirect block, in addition to direct blocks and a singly-indirect block.You’ll have to have only 11 direct blocks, rather than 12, to make room for your new doubly-indirect block; you’re not allowed to change the size of an on-disk inode.
-The first 11 elements of
+ip->addrs[]
should be direct blocks; the 12th should be a singly-indirect block (just like the current one); the 13th should be your new doubly-indirect block. You are done with this exercise whenbigfile
writes 65803 blocks andusertests
runs successfully.+-cat 是 Linux 下的一个常用命令,功能是将文件的内容打印到标准输出。
+它核心实现大体如下:
+
int main(int argc, char* argv[])
{
char buf[513] = {'\0'};
int nread;
int fd = open(argv[1], O_RDONLY, 0);
while(nread = read(fd, buf, 512))
{
buf[nread] = '\0';
puts(buf);
}
return 0;
}感想
意外地很简单()在此不多做赘述,直接上代码。
-唯一要注意的一点就是记得在
-itrunc
中free掉-
代码
修改定义
- -
// in fs.h
// On-disk inode structure
struct dinode {
// ...
uint addrs[NDIRECT+2]; // Data block addresses
};- -
// in file.h
// in-memory copy of an inode
struct inode {
// ...
uint addrs[NDIRECT+2];
};修改bmap()
+
// in fs.c
// 调试用
static int cnt = 0;
static uint
bmap(struct inode *ip, uint bn)
{
uint addr, *a;
struct buf *bp;
if(bn < NDIRECT){
if((addr = ip->addrs[bn]) == 0)
ip->addrs[bn] = addr = balloc(ip->dev);
return addr;
}
bn -= NDIRECT;
if(bn < NINDIRECT){
// Load indirect block, allocating if necessary.
if((addr = ip->addrs[NDIRECT]) == 0)
ip->addrs[NDIRECT] = addr = balloc(ip->dev);
bp = bread(ip->dev, addr);
a = (uint*)bp->data;
if((addr = a[bn]) == 0){
a[bn] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
// CODE HERE
bn -= NINDIRECT;
if(bn < NDOUBLEINDIRECT){
// 调试用
if(bn/10000 > cnt){
cnt++;
printf("double_indirect:%d\n",bn);
}
// 第一层
if((addr = ip->addrs[NDIRECT+1]) == 0)
ip->addrs[NDIRECT+1] = addr = balloc(ip->dev);
// 第二层
bp = bread(ip->dev,addr);
a = (uint*)bp->data;
if((addr = a[(bn >> 8)]) == 0){
a[(bn >> 8)] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
// 第三层
bp = bread(ip->dev,addr);
a = (uint*)bp->data;
if((addr = a[(bn & 0x00FF)]) == 0){
a[(bn & 0x00FF)] = addr = balloc(ip->dev);
log_write(bp);
}
brelse(bp);
return addr;
}
panic("bmap: out of range");
}在cat的代码中,open函数返回了psinfo的文件描述符,read函数读到该文件描述符,就会识别出我们要读写的文件是PROC类型的,因此就会跳转到我们的proc_handler去执行,再进一步跳转到psinfo_handler执行。根据cat的代码和指导书的提示,不难得出,我们的目标就是把进程的信息按照格式给弄进buf里面,就可以了。
+而这也正体现了proc作为“**虚拟文件**”的特点。对它进行读写,它的信息并非存放在磁盘中,而是全部由放在内存中的逻辑和数据【由task_struct提供】来完成。
+在fs文件夹下创建文件proc_dev.c,编写proc文件的处理函数。代码如下:
+-
struct task_struct** p=&FIRST_TASK;
char s[100];
int flag;
extern int psinfo_handler(off_t* f_pos,char* buf);
extern int hdinfo_handler(off_t* f_pos,char* buf);
int proc_handler(unsigned short dev,char* buf,int count,off_t* f_pos){
//根据设备编号,把不同的内容写入到用户空间的 buf
switch(dev){
case 0:
return psinfo_handler(f_pos,buf);
case 1:
return hdinfo_handler(f_pos,buf);
default:
break;
}
return -1;
}
//在内核态和用户态间传递数据
int put_into_buf(char* buf,char* s){
int cnt=0;
while(s[cnt]!='\0'){
put_fs_byte(s[cnt++],buf++);
}
return cnt;
}
int sprintf(char* buf,const char* fmt,...){
va_list args;int i;
va_start(args,fmt);
i=vsprintf(buf,fmt,args);
va_end(args);
return i;
}
int psinfo_handler(off_t* f_pos,char* buf){
int i;
//初始化字符串
for(i=0;i<100;i++) s[i]=0;
//如果是第一次read,需要在屏幕上打印列表头,并且重置p指针为进程队列头
if((*f_pos)==0){
sprintf(s,"pid\tstate\tfather\tcounter\tstart_time\n");
p=&FIRST_TASK;
}
//到达文件末尾
if((*p)==NULL){
return 0;
}
//每次仅输出一行
if((*f_pos)!=0){
sprintf(s,"%ld\t%ld\t%ld\t%ld\t%ld\n",(*p)->pid,(*p)->state,(*p)->father,(*p)->counter,(*p)->start_time);
p++;
}
int cnt=put_into_buf(buf,s);
*f_pos+=cnt;
return cnt;
}
//可参考fs/super.c mount_root()
int hdinfo_handler(off_t* f_pos,char* buf){
//防止循环多次打印
if(flag==1){
flag=0;
return -1;
}
struct super_block* sb;
sb=get_super(0x301);/*磁盘设备号 3*256+1*/
int free=0;
int i=sb->s_nzones;
while (-- i >= 0)
if (!set_bit(i&8191,sb->s_zmap[i>>13]->b_data))
free++;
sprintf(s,"total_blocks:\t%d\nfree_blocks:\t%d\nused_blocks:\t%d\n",sb->s_nzones,free,sb->s_nzones-free);
int cnt=put_into_buf(buf,s);
flag=1;
return cnt;
}修改itrunc
+
// Truncate inode (discard contents).
// Caller must hold ip->lock.
void
itrunc(struct inode *ip)
{
int i, j;
struct buf *bp;
uint *a;
for(i = 0; i < NDIRECT; i++){
if(ip->addrs[i]){
bfree(ip->dev, ip->addrs[i]);
ip->addrs[i] = 0;
}
}
if(ip->addrs[NDIRECT]){
bp = bread(ip->dev, ip->addrs[NDIRECT]);
a = (uint*)bp->data;
for(j = 0; j < NINDIRECT; j++){
if(a[j])
bfree(ip->dev, a[j]);
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT]);
ip->addrs[NDIRECT] = 0;
}
// CODE HERE
if(ip->addrs[NDIRECT+1]){
bp = bread(ip->dev, ip->addrs[NDIRECT+1]);
a = (uint*)bp->data;
// 双层循环
for(j = 0; j < NINDIRECT; j++){
if(a[j]){
struct buf* tmp_bp = bread(ip->dev,a[j]);
uint* tmp_a = (uint*)tmp_bp->data;
for(int k = 0;k < NINDIRECT; k++){
if(tmp_a[k])
bfree(ip->dev,tmp_a[k]);
}
brelse(tmp_bp);
bfree(ip->dev,a[j]);
}
}
brelse(bp);
bfree(ip->dev, ip->addrs[NDIRECT+1]);
ip->addrs[NDIRECT+1] = 0;
}
ip->size = 0;
iupdate(ip);
}运行结果:
+-
Symbolic links
--In this exercise you will add symbolic links to xv6.
-Symbolic links (or soft links) refer to a linked file by pathname; when a symbolic link is opened, the kernel follows the link to the referred file.
-Symbolic links resembles hard links, but hard links are restricted to pointing to file on the same disk, while symbolic links can cross disk devices.
-Although xv6 doesn’t support multiple devices, implementing this system call is a good exercise to understand how pathname lookup works.
-You will implement the
-symlink(char *target, char *path)
system call, which creates a new symbolic link at path that refers to file named by target. For further information, see the man page symlink. To test, add symlinktest to the Makefile and run it.- --硬链接不会创建新的物理文件,但是会使得当前物理文件的引用数加1。当硬链接产生的文件存在时,删除源文件,不会清除实际的物理文件,即对于硬链接“生成的新文件”不会产生任何影响。
-软链接就更像一个指针,只是指向实际物理文件位置,当源文件移动或者删除时,软链接就会失效。
-【所以说,意思就是软链接不会让inode->ulinks++的意思?】
-感想
这个实验比上个实验稍难一些,但也确实只是moderate的水平,其复杂程度主要来源于对文件系统的理解,还有如何判断环,以及对锁的获取和释放的应用。我做这个实验居然是没看提示的【非常骄傲<-】,让我有一种自己水平上升了的感觉hhh
-正确思路
本实验要求实现软链接。首先需要实现创建软链接:写一个系统调用
-symlink(char *target, char *path)
用于创建一个指向target的在path的软链接;然后需要实现打开软链接进行自动的跳转:在sys_open
中添加对文件类型为软链接的特殊处理。初见思路
我的初见思路是觉得可以完全参照
-sys_link
来写。但其实还是很不一样的。-
sys_link
的逻辑:-
-- 获取old的inode
-- 获取new所在目录的inode,称为dp
-- 在dp中添加一项entry指向old
--
sys_symlink
的逻辑:-
-- -
通过path创建一个新的inode,作为软链接的文件
-这里选择新建inode,而不是像link那样做,主要还是为了能遵从
-symlinktest
给的接口使用方法(朴实无华的理由)。而且这么做也很方便,符合“一切皆文件”的思想,也能简单化对其在open
中的处理。- -
在inode中填入target的地址
-我们可以把软链接视为文件,文件内容是其target的path。
-可以说是毫不相干,所以还是直接自起炉灶比较好。
-一些错误
其实没什么好说的,虽然debug过程挺久,但是靠常规的printf追踪就都可以看出来是哪里错了。下面我说说一个我印象比较深刻的吧。
--
symlinktest
中有一个检测点是,软链接不能成环,也即b->a->b是非法的。于是,我就选择了用快慢指针来检测环形链表这个思想,用来看是否出现环。在
-symlinktest
的另一个检测点中:-
我出现了如下错误:
--
此时的结构是1[27]->2[28]->3[29]->4,[]内为inode的inum。
-快慢指针的实现方式是当cnt为奇数的时候,慢指针才会移动。而上图中,cnt==0时,两个指针的值都发生了变化,这就非常诡异。
-这其实是因为slow指针所指向的那个inode被释放了,然后又被fast指针的下一个inode捡过来用了,从而导致值覆盖。
-为什么会被释放呢?
-+
// 快指针移动
readi(ip,0,(uint64)path,0,MAXPATH);
iunlock(ip);
if((ip = namei(path)) == 0){
end_op();
return -1;
}
// 在这里!!!
ilockput(ip);这部分踩过的坑:
1.LAST_TASK 的定义
+对于LAST_TASK,我本来的理解是,当前所有进程的最后一个。
+本来我设的是跟schedule一样,另p=LAST_TASK,从末尾开始打印。我那时其余代码跟上面一样,就只是把上面的FIRST改成LAST,结果输出为空,调试发现LAST_TASK==NULL。
+然后打开sched.h,看到LAST_TASK的定义:
+-
在这里,我错误地调用了
-ilockput
,从而使inode的ref–,使得它在下一次fast指针调用namei
,namei
调用iget
时,该inode被当做free inode使用,于是就这么寄了。所以我们需要把
-ilockput
的调用换成ilock
,这样一来就能防止inode被free。至于什么时候再iput?我想还是交给操作系统启动时的清理工作来做吧23333【开摆】代码
-
添加定义
fcntl.c
open参数
-+
// 意为只用获取软链接文件本身,而不用顺着软链接去找它的target文件原来它就是单纯简单粗暴地指“最后一个”进程23333
+我们目前当前的进程数量远远小于进程的最大数量,因此最大数量编号的那个进程自然也就是空的了。
+2.char s[100]={0};
+用这个的时候编译报错:undefined reference to ’memset‘
+说明这个简略写法其实本质是用的memset,而要用memset的话需要包含头文件string.h。经测试得包含了string.h后确实就好使了。
+-
//s_imap_blocks、ns_zmap_blocks、
//total_blocks、free_blocks、used_blocks、total_inodes
for(i=0;is_zmap_blocks;i++)
{
bh=sb->s_zmap[i];
db=(char*)bh->b_data;
for(j=0;j<1024;j++){
for(k=1;k<=8;k++){
if((used_blocks+free_blocks)>=total_blocks)
break;
if( *(db+j) & k)
used_blocks++;
else
free_blocks++;
}stat.h
文件类型
-+
3.我发现一件事
+我第一次把init/main.c写错了,写成:
+-
mkdir("/proc",0755);
mknod("/proc/psinfo",S_IFPROC|0400,0);
mknod("/proc/hdinfo",S_IFPROC|0400,0);
mknod("/proc/inodeinfo",S_IFPROC|0400,0);添加sys_symlink系统调用
+
// in sysfile.c
uint64
sys_symlink(void)
{
char target[MAXPATH], path[MAXPATH];
struct inode *ip;
if(argstr(0, target, MAXPATH) < 0 || argstr(1, path, MAXPATH) < 0)
return -1;
begin_op();
// 创建软链接结点
ip = create(path,T_SYMLINK,0,0);
//printf("symlink:before writei,inum = %d\n",ip->inum);
// 此处可以防止住一些并发错误
if(ip ==0){
end_op();
return 0;
}
// 向软链接结点文件内写入其所指向的路径
writei(ip,0,(uint64)target,0,MAXPATH);
//printf("symlink:after writei\n");
// 软链接不需要让nlink++
// 记得要释放在create()中申请的锁
iunlockput(ip);
end_op();
return 0;
}设别号忘了改了。然后进行了一次编译,运行。
+之后我发现错了,就改成了
+-
mkdir("/proc",0755);
mknod("/proc/psinfo",S_IFPROC|0400,0);
mknod("/proc/hdinfo",S_IFPROC|0400,1);
mknod("/proc/inodeinfo",S_IFPROC|0400,2);修改open
+
uint64
sys_open(void)
{
// ...
begin_op();
if(omode & O_CREATE){
ip = create(path, T_FILE, 0, 0);
if(ip == 0){
end_op();
return -1;
}
} else {
// 软链接不可能是以O_CREATE的形式创建的
if((ip = namei(path)) == 0){
end_op();
return -1;
}
ilock(ip);
if(ip->type == T_DIR && omode != O_RDONLY){
iunlockput(ip);
end_op();
return -1;
}
// 修改从这里开始
// 快慢指针
// ip为快指针,slow为慢指针
uint cnt = 0;
struct inode* slow = ip;
// 可能有多重链接,因而需要持续跳转
while(ip->type == T_SYMLINK){
//printf("slow = %d,fast = %d,cnt = %d\n",slow->inum,ip->inum,cnt);
// 其实这个只需要检测一次就够了。但为了编码方便,仍然把它保留在while循环中
if(omode & O_NOFOLLOW){
break;
}else{
// 检测到cycle
if(slow == ip && cnt!=0){
iunlockput(ip);
end_op();
return -1;
}
// 快指针移动
readi(ip,0,(uint64)path,0,MAXPATH);
// 此处不能用iunlockput(),具体原因见 感想-一些错误
iunlock(ip);
if((ip = namei(path)) == 0){
end_op();
return -1;
}
ilock(ip);
// 慢指针移动
// 注意,我慢指针移动的时候没有锁保护,因为用锁太麻烦了()其实还是用锁比较合适
if(cnt & 1){
//printf("%d\n",cnt);
readi(slow,0,(uint64)path,0,MAXPATH);
if((slow = namei(path) )== 0){
end_op();
return -1;
}
}
cnt++;
}
}
// 当跳出循环时,此时的ip必定是锁住的
}
if(ip->type == T_DEVICE && (ip->major < 0 || ip->major >= NDEV)){
iunlockput(ip);
end_op();
return -1;
}
// ...
}再次编译运行,结果上面的那个错还是没改回来
+直到我手动把proc文件夹删了,再重新读一次磁盘加载proc文件夹,才回归正常。
+感想
本次实验耗时:下午一点到晚上九点半()
+本实验通过对proc虚拟文件的编写流程,实际上让我们体会到了“一切皆文件”的思想。
+什么东西都可以是文件,只不过它们有不同的文件类型和不同的read/write处理函数。
+对于终端设备和磁盘,其read/write函数本质上是在用out指令跟它的缓冲区交互,只不过磁盘比终端设备抽象层次更深,包含了文件系统的层层封装。
+对于虚拟文件,其read/write函数本质上就是与内存交互,通过一段逻辑【处理函数】将内存存储的当前操作系统信息实时显示出来,而不需要存储。
+还有,参考文章那篇的代码写的很好,快去看!
+]]>
-+The
-mmap
andmunmap
system calls allow UNIX programs to exert detailed control over their address spaces.They can be used to:
--
-- share memory among processes
-- map files into process address spaces
-- as part of user-level page fault schemes such as the garbage-collection algorithms discussed in lecture.
-In this lab you’ll add
-mmap
andmunmap
to xv6, focusing on memory-mapped files.mmap是系统调用,在用户态被使用。我们这次实验仅实现mmap功能的子集,也即memory-mapped files。
-
看到题目时,我首先联想到的是mealy型状态机,因为我联想到了序列检测。课内的序列检测讲的时候是把它当做mealy型的。但看了标准作答之后,才发现它其实应该是moore型。这让我对这二者的区别产生了深深的不解。
+原来对于moore型状态机和mealy型状态机的理解仅仅停留在概念上,“moore型状态机的输出与输入无关,只与当前状态有关”“mealy型状态机输出与输入和现态都有关”。但这其实是一句非常抽象的话:什么是“无关”,什么是“有关”?moore型状态机的状态不也是依据输入进行转移的吗?那么这算不算“有关”?
+探究之后,我得到了更精确的“有关”“无关”的定义。
-declaration for
-mmap
:+ +
void *mmap(void *addr, size_t length, int prot, int flags,
int fd, off_t offset);-
-
-- -
参数
--
-- -
-
addr
is always zero.You can assume that
-addr
will always be zero, meaning that the kernel should decide the virtual address at which to map the file.【addr
由kernel决定,因而用户态只需传入0即可】- -
-
length
is the number of bytes to mapMight not be the same as the file’s length.
-- -
-
prot
indicates whether the memory should be mapped readable, writeable, and/or executable.you can assume that
-prot
isPROT_READ
orPROT_WRITE
or both.- -
-
flags
has two values.-
-- -
-
MAP_SHARED
meaning that modifications to the mapped memory should be written back to the file,
-如果标记为此,则当且仅当file本身权限为RW或者WRITABLE的时候,prot才可以标记为PROT_WRITE
-- -
-
MAP_PRIVATE
meaning that they should not.
-如果标记为此,则无论file本身权限如何,prot都可以标记为PROT_WRITE
-- -
You can assume
-offset
is zero (it’s the starting point in the file at which to map)- -
return
--
mmap
returns that kernel-decided address, or 0xffffffffffffffff if it fails.如果两个进程同时对某个文件进行memory map,那么这两个进程可以不共享物理页面。
+![]()
---
munmap(addr, length)
should remove mmap mappings in the indicated address range.If the process has modified the memory and has it mapped
-MAP_SHARED
, the modifications should first be written to the file. 【如果两个进程的修改发生冲突了怎么办?】An
+munmap
call might cover only a portion of an mmap-ed region, but you can assume that it will either unmap at the start, or at the end, or the whole region (but not punch a hole in the middle of a region).来自:Moore状态机和Mealy状态机的区别(以序列检测器为例)
+Moore状态机输出只与此时的状态有关,因此假如需要检测宽度为4的序列,则需要五个状态。
+Mealy状态机输出与此时的状态以及输入有关,因此假如需要检测宽度为4的序列,只需要四个状态即可。
这个实验做得我……怎么说,感觉非常地难受吧。虽然我认为我这次做得挺不错的,因为我没有怎么看hints,我的代码差不多全都是我自己想出来的,没有依赖保姆级教学,我认为是一个很好的进步。不过,正因为我没有看hints,导致我的想法比起答案来思路非常地奇诡,导致我第一次错误想法写了一天,看了hints后决心痛改前非,结果第二次错误想法又写了一天emmm
-下面的第一个代码版本虽然可以过掉mmaptest,但确实还是有一个很致命的bug,并且lazy也没有lazy到位,最后的版本离正确思路还有偏差,也就是下面放的第一个代码版本是错误的,但我认为它也不是完全没有亮点。第二个版本才是经过改正的正确版本,但写得着实有点潦草。
-笔记整理得也有点匆忙,毕竟我真的话比较多而且心里很烦。总之,先记录我的全部思路过程,至于价值如何,先不管了2333
-所以说,我们要做的,就是实现一个系统调用mmap,在mmap中,应该首先申请几页用来放file的内容,并且在页表中填入该项,然后再返回该项的虚拟地址。然后在munmap中,再将该file页内容写入file。
-也就是说,直接在mmap把文件的全部内容写入内存,然后各进程读写自己的那块内容块,最后在munmap的时候把修改内容写入文件然后释放该内存块就行了
-题目要求the kernel should decide the **virtual address** at which to map the file.
也就是说,在我们的mmap
中,需要决定我们要讲文件内容放在哪里。那要放在哪呢……
我第一反应很奇葩:扫描页表,找到空闲页。但我自己也知道这样不可行,文件内容不止一页,这种零零散散存储需要的数据结构实现起来太麻烦了。
-那怎么办?可以在heap内分配。那么到底怎么样才能在heap里分配?你该怎么知道heap哪里开始是空闲的,哪里是用过的,不还是得扫描页表吗?【思维大僵化】
-其实……道理很简单。我们之间把proc->sz
作为mapped-file的起始地址就好了。相信看到这里,你也明白这是什么原理了。能想到这个,我感觉确实很不容易。
初见思路虽然简单,但是很粗暴,如果文件很大,宝贵的内存空间就会被我们浪费。所以我们借用lazy allocation的思想,先建立memory-file的映射,再在缺页中断中通过文件读写申请内存空间,把文件内容读入内存。
-问题就在于如何“先建立memory-file的映射”。在lazy allocation中,我们是先填好所有的对应页表项,仅是不申请对应的物理内存,也即占着XX不XX。在这次实验中,我们也是这么做,只不过新增了一个难点,那就是如何管理这些页。因为lazy allocation页与页之间没有比较紧密的关系,但是在mmap中页却可以被所属文件这个关键字划分。因而,我们需要一个数据结构,来给页分门别类地组织在一起,并且记录它们的meta data比如说所属文件之类的,这也就是hints里的VMA结构,也即我的filemap结构。
-我们可以将这样的数据结构池化,并且存储在proc域中,以避免对象的重复创建。
+联想到我们课上学习的序列检测:
+它这明明长度为3的序列用了4个状态,应该算是moore型,为什么我们却被教说序列检测器是mealy型状态机呢?
+原因是因为,我们进行了状态化简这一步,将moore型状态机转化为了mealy型状态机。
--我的lazy法与别人不大一样……我没有想得像他们那么完美。我的做法是,在需要读某个地址的文件内容时,直接确保这个地址前面的所有文件内容都读了进来。也即在filemap中维护一个okva,表明va
-okva这段内存已经读入,之后就仅需再读入okvaneed_va这段地址就行。这样虽然lazy了,但没完全lazy。我认为这不能体现lazy的思想……因为一读读一坨,还是很占空间啊。
+这俩是可以相互转化的
+ +把Moore机转换为Mealy机的办法为,把次态的输出修改为对应现态的输出,同时合并一些具有等价性能的状态。把Mealy机转换为Moore机的办法是,把当前态的输出修改为对应次态的输出,同时添加一些状态。如图1所示,为把Mealy机状态图转化为Moore机状态图。
++
图1 Mealy型机转换为Moore型机如图1所示,把Mealy型机转换为Moore型机,只要把现时输出改变为下一时刻输出。对于状态A,有4个箭头指向它,表示在当前状态下有4个状态可以转换为下一状态的A;同时当前输出均为0,可以把0移入状态A内部,表示在Moore机中状态A的输出为0。同理,可以把0分别移位B/C状态。但对于状态D,有两个箭头指向且具有不同的输出值,需要把状态D分解成两个状态D1和D2(每个状态对应一个输出,当输出不同需要利用不同的状态表示,这即是Moore机具有更多状态的原因),得到完整的Moore机状态模型。
+同理,若把上图的Moore机转换为Mealy机,只要把Moore机中下一状态的输出改变成Mealy机中当前状态的输出,由于D1/D2两状态处于A/C两状态之间,且相当于A/C节点之间的一个等效节点,可以把D1/D2两状态合并为一个状态。
因而,我们需要做的就是:
-在mmap中将信息填入该数据结构
-在我的代码中,还针对proc->sz不满足page-align做出了对策:先把文件的PGROUNDUP(sz)-sz
这部分的信息读入,并且更新okva,这样一来,之后在usertrap中,就可以从okva开始一页页地分配地址,做到自然地page-align了。
--为什么要对不满足page-align的情况进行处理?
-这是因为,growproc的时候一次性扩充一页,但proc->sz却可以不满足page-align,也就是说,proc->sz所处的这一页已经被分配了。
-在我们的lazy思路中,我们如果不预先读入文件页,是只能等待用户陷入缺页中断的情况下才能读入文件内容。
-但是,proc->sz这一页已经被分配了。因而,在用户态读取这一页地址的时候,并不会发生缺页中断。因而,就会发生文件内容未读入,用户读到脏数据的情况。
-其实还有一种更简单的办法可以强制page-align,那就是,直接让起始地址为
+ +PGROUNDUP(proc->sz)
……至于为什么我不用这个,而要写这么多麻烦的东西呢?答案是我没想到。()并非所有时序电路都可以使用Mealy模型实现。 一些时序电路只能作为摩尔机器实现。
在usertrap增加对缺页中断的处理
-readi
(正确)fileread
(错误)读取文件内容并存入物理内存在munmap中进行释放
+所以,我们可以出此暴论:在课程范围内,首先以moore的思想来设计状态机。如果该状态机可以被化简,那么这道题就要用mealy型的来做;如果不能,那么这道题就是得用moore型状态机来做。
+一开始的那个时序锁的moore状态机不能化简,因此它是moore型。
+++]]>这个点本来可以讲得更清楚一些的……只教会我们做题的套路有啥意思呢←_←
+
在传统的BIOS系统中,计算机具体的启动流程如下:
修改fork和exit
+在本次内核编译配置过程中,最主要探究的是文件系统的装载过程,也即介于6-7之间的部分。
+文件系统在启动流程中的发展历程可以分为以下三个部分:
exit
-手动释放map-file域
--+为什么不能把这些合并到
-wait
中调用的freepagetable
进行释放呢?因为
-freepagetable
只会释放对应的物理页,没有达到munmap
减少文件引用等功能。
GRUB文件系统
+由 GRUB 自身通过 BIOS 提供的服务加载
fork
-手动复制filemap池
+initramfs
+由GRUB加载,用于挂载真正的文件系统
真正的根文件系统
上面说到:
---问题就在于如何“先建立memory-file的映射”。在lazy allocation中,我们是先填好所有的对应页表项,仅是不申请对应的物理内存,也即占着XX不XX。在这次实验中,我们也是这么做,只不过新增了一个难点,那就是如何管理这些页。因为lazy allocation页与页之间没有比较紧密的关系,但是在mmap中页却可以被所属文件这个关键字划分。因而,我们需要一个数据结构,来给页分门别类地组织在一起,并且记录它们的meta data比如说所属文件之类的,这也就是hints里的VMA结构,也即我的filemap结构。
-
官方给出的答案是在proc域里的pool。我……额……是把这些信息,存入在页中(真是自找麻烦呀)
-具体来说,就是,我在mmap
的时候给每个文件申请一页,然后在页的开头填上和filemap结构相差无几的那些参数,再加上一个next指针,表示下一个文件页的地址。页的剩下部分就用来存储数据。总的就是一个链表结构。
这个思路其实很不错,比起上面的直接在proc内存的尾巴扩容,这个空间利用率应该更大,并且不仅能节省物理内存,还能节省虚拟地址空间,实现了lazy上加lazy。
-但问题是……我为什么非要傻瓜式操纵内存,在页的开头填入参数数据,而不是把这种页抽象为一个个node,最终形成一个十字链表的形式(差不多的意思,鱼骨状),组织进proc域,这样不挺好的吗……唔,有时候我头脑昏迷程度让我自己都感到十分震惊。归根结底,还是想得太少就动手了,失策。
-总之放上代码。我没有实现next指针,仅假设文件内容不超过一页。也就是这一页开头在mmap中填meta data,其余部分在usertrap中填入文件内容。【这个分开的点也让我迷惑至极……】
-
|
} else if(r_scause() == 13 || r_scause() == 15){ |
正如开头所说的那样,我并没有完美做好这次实验,下面代码有一个致命的bug。
-先说说致命bug是什么。
-我的filemap结构体其实隐藏了两个具有“offset”这一含义的状态。一个是filemap里面的成员变量offset,另一个是filemap里面的成员变量file的成员变量off:
-// in proc.h |
在我的代码里,它们被赋予了不同的含义。
-filemap->file->off
被用于trap.c
中,表示的是当前未读入文件内容的起始位置(实际上也就是okva-va
的值),用于自然地使用fileread
进行文件读入。
--比如说,这次读入PGSIZE,那么off就会在
-fileread
中自增PGSIZE。下次调用fileread
就可以直接从下一个位置读入了,这样使代码更加简洁
filemap->offset
被用于munmap
中。filewrite
同fileread
一样,都是从file->off
处开始取数据。munmap
所需要取数据的起始位置和trap.c
中需要取数据的起始位置肯定不一样,
--想想它们的功能。
-trap.c
的off需要始终指向有效内存段的末尾,但munmap
由于要对特定内存段进行写入文件操作,因而off要求可以随机指向。
因而,我们可以将当前va对应的文件位置记录在offset中。届时,我们只需要从p->filemaps[i].offset+va-p->filemaps[i].va
取数据就行。
上述两个变量相辅相成,看上去似乎能够完美无缺地实现我们的功能。但是,实际上,不行。为什么呢?因为它们的file指针,filemap->file
,如果被两个mmap区域同时使用的话,就会出问题。
可以来看看mmaptest.c
中的这一段代码:
makefile(f); |
// in trap.c |
这段代码因为共用fd,导致file指针被两个mmap区域同时使用。
--共用fd,为什么file指针也一起共用了?
-可以追踪一下它们的生命周期:
-- -
// in sys_open()
// 获取file结构体和文件描述符。
if((f = filealloc()) == 0 || (fd = fdalloc(f)) < 0){
// in sysfile.c
// Allocate a file descriptor for the given file.
// Takes over file reference from caller on success.
static int
fdalloc(struct file *f)
{
int fd;
struct proc *p = myproc();
for(fd = 0; fd < NOFILE; fd++){
if(p->ofile[fd] == 0){
p->ofile[fd] = f;
return fd;
}
}
return -1;
}可以看到,它实际上是有一个文件描述符表,key为fd,value为file指针。因而,同一张表,fd相同,file指针相同。
-注:父子进程,同样的fd,file指针也是相同的
-fork出来的父子进程同一个句柄对同一个文件的偏移量是相同的,这个原理应该是因为,父子进程共享的是文件句柄这个结构体对象本身,也就是拷贝的时候是浅拷贝而不是深拷贝。
-+
// in fork()
// increment reference counts on open file descriptors.
for(i = 0; i < NOFILE; i++)
if(p->ofile[i])
np->ofile[i] = filedup(p->ofile[i]);下面,将介绍1和2两个流程。
+GRUB
+-GRUB(GNU GRand Unified Bootloader)是一种常用的引导加载程序,用于在计算机启动时加载操作系统。
+GRUB的主要功能是在计算机启动时提供一个菜单,让用户选择要启动的操作系统或内核。它支持多个操作系统,包括各种版本的Linux、Windows、BSD等。通过GRUB,用户可以在多个操作系统之间轻松切换。
+除了操作系统选择,GRUB还提供了一些高级功能,例如引导参数的设置、内存检测、系统恢复等。它还支持在启动过程中加载内核模块和初始化RAM磁盘映像(initrd或initramfs)。
+GRUB具有高度可配置性,允许用户自定义引导菜单、设置默认启动项、编辑内核参数等。它还支持引导加载程序间的链式引导,可以引导其他引导加载程序,如Windows的NTLDR。
最后的
-check that the parent's mappings are still there.
环节中,_v1(p1)
执行时并没有陷入trap,这是正常的。不正常的是_v1(p2)
的执行结果。它陷入了trap,但是却因file->off == file size
,导致被判定为已全部读入文件,事实上却是并没有读入文件。为什么会这样呢?
-这是因为p1和p2共用同一个fd,也就共用了同一个file指针。共用了一个file指针,那么p1和p2面对的
-file->off
相同。上面说到,file->off
用于控制文件映射。那么,当p1完成了对文件的映射,p1的off指针如果不加重置,就会永远停留在file size处。这样一来,当p2想要使用同样的file指针进行文件映射时,就会出问题。这个问题的一个解决方法是每次
-mmap
都深拷贝一个船新file结构体。但是这样的话,file
域里的ref
变量就失去了它的意义,并且file对象池应该也很快就会爆满,非常不符合设计方案。这个问题的完美解,是不要赋予
-file->off
这个意义,而是使用readi
替代fileread
。+
fileread(struct file *f, uint64 addr, int n)
readi(struct inode *ip, int user_dst, uint64 dst, uint off, uint n)GRUB的基本作用流程为:
++
+- BIOS加载MBR,MBR加载GRUB,开始执行GRUB程序
+- GRUB程序会读取
+grub.cfg
配置文件- GRUB程序依据配置文件,进行内核的加载、根文件系统的挂载等操作,最后将主导权转交给内核
+grub.cfg
内核启动时,GRUB程序会读取
+/boot/grub/
目录下的GRUB配置文件grub.cfg
,其中记录了所有GRUB菜单可供选择的内核选项(menuentry)及其对应的启动依赖参数。以6.4.0内核选项为例:-
menuentry标识着GRUB菜单中的一个内核选项
menuentry 'Ubuntu' --class ubuntu --class gnu-linux --class gnu --class os $menuentry_id_option 'gnulinux-simple-XXX' {
recordfail # 记录上次启动是否失败,用于处理启动失败的情况
load_video # 加载视频驱动模块,用于在启动过程中显示图形界面
gfxmode $linux_gfx_mode # 设置图形模式
insmod gzio # 加载gzio模块,提供对GZIP压缩和解压缩功能的支持
# 如果是在Xen虚拟化平台上,则加载xzio和lzopio模块
if [ x$grub_platform = xxen ]; then insmod xzio; insmod lzopio; fi
insmod part_gpt # 加载part_gpt模块,支持GUID分区表(GPT)
insmod ext2 # 加载ext2模块,支持ext2文件系统
# 设置文件系统的根分区
set root='hd0,gpt3'
if [ x$feature_platform_search_hint = xy ]; then
search --no-floppy --fs-uuid --set=root --hint-bios=hd0,gpt3 --hint-efi=hd0,gpt3 --hint-baremetal=ahci0,gpt3 XXX
else
search --no-floppy --fs-uuid --set=root XXX
fi
linux /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro text # 指定内核映像的路径和启动参数
initrd /boot/initrd.img-6.4.0-rc3+ # 指定initramfs映像的路径
}这样做的好处是,我们可以实时计算offset(前面提到,其恰恰等于okva-va),而不用把这个东西用file的off来表示。
---也确实,我之所以弯弯绕绕那么曲折,是因为只想到了
-fileread
这个函数,压根没注意到还有一个readi
……我在下面的代码仅做了一个能够通过测试,但是上面的bug依然存在的功利性折中代码。我是这么实现的:
-+
// 在`mmap`的时候初始化`file->off`
p->filemaps[i].file->off = offset;
// 在`munmap`的时候清零`file->off`
p->filemaps[i].file->off = 0;可以看到,
+grub.cfg
主要记录了一些该内核启动需要的依赖module,以及内核映像和initramfs映像的路径。menuentry的代码中,有以下几个要点值得注意:
++
+- +
+
insmod gzio
由于加载gzio模块,提供对GZIP压缩和解压缩功能的支持。
+看到这里我第一反应是觉得有点割裂,为啥这看着比较无关紧要的解压缩功能要在内核启动之前就需要有呢?于是我想起来在配置内核时,有一个选项是这样的:
++
在配置选项中,我们选择了对initramfs的支持,并且勾选了
+Support initial ramdisk/ramfs compressed using gzip
,也即在编译时通过gzip压缩initramfs的大小以节省空间。所以说,我们在内核启动之前,持有的initramfs处于被压缩的状态。故而,我们自然需要在内核启动之前安装gzio模块,从而支持之后对initramfs的解压缩了。
+- +
+
insmod ext2
这句代码说明,GRUB的临时文件系统为ext2类型,这句代码事实上是在安装GRUB建立临时文件的必要依赖包,从而GRUB程序之后才能建立其临时文件系统、从/boot/initrd.img获取initramfs映像。
+- +
+
linux /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro text
指定了启动参数,也即将根文件系统以只读(
+ro
)的方式挂载在root=UUID=XXX
对应的块设备上,并且默认以text
方式(也即非图形化的Shell界面)启动内核。此处的启动参数可在下一个部分介绍的
+grub
文件中个性化。grub.cfg的生成与修改
实际运用中,很多时候需要对启动参数进行一些修改。下面介绍两种修改
+grub.cfg
的方法。/etc/default/grub
可以看到,
+grub.cfg
其实格式较为固定(也即由一系列内容也比较相似的menuentry构成)。因而,实际上我们是通过grub.d
生成grub.cfg
的(6.S081实验中事实上也涉及了这一点),而/etc/default/grub
则是GRUB程序以及grub.cfg
生成的配置文件。下面介绍下该文件主要有哪些配置选项。+
If you change this file, run 'update-grub' afterwards to update
/boot/grub/grub.cfg.
For full documentation of the options in this file, see:
info -f grub -n 'Simple configuration'
开机时GRUB界面的持续时间,此处设置为30s
GRUB_TIMEOUT=30
GRUB_CMDLINE_LINUX=""
不使用图形化界面
GRUB_TERMINAL=console
图形化界面的大小
GRUB_GFXMODE=640x480
不使用UUID
GRUB_DISABLE_LINUX_UUID=true
隐藏recovery mode
GRUB_DISABLE_RECOVERY="true"重点看下这几个参数:
++
+
GRUB_CMDLINE_LINUX
表示最终生成的grub.cfg中的每一个menuentry中的linux那一行需要附加什么参数。
+例如说,如果设置为:
++
表示initramfs在挂载真正的根文件系统之前,需要等待120s,用于防止磁盘没准备好导致的挂载失败
GRUB_CMDLINE_LINUX="rootdelay=120"那么,最终在menuentry中的启动参数就为:
+-
linux /boot/vmlinuz-6.4.0-rc3+ root=UUID=XXX ro rootdelay=120 text因而,结论是,一步错步步错,一个错误需要更多的错误来弥补,最后还是错的(悲)
-如何把下面的错误思路改成正确思路
可以做以下几点:
--
-- -
正确地lazy
-每次trap仅分配一页。
-- +
改用readi函数,修改
+file->off
的语义其他一些常见的选项:
+
直接以路径来标识块设备而非使用UUID。此为old option,建议尽量使用UUID
GRUB_CMDLINE_LINUX="root=/dev/sda3"
标明init进程(启动后第一个进程)的具体路径。此处指明为`/bin/sh`
GRUB_CMDLINE_LINUX="init=/bin/sh"+
GRUB_DEFAULT
参考 可以用来指定重启时的内核选项。如
GRUB_DEFAULT="1> 0"
表示选择第一个菜单界面的第2栏(Advanced for Ubuntu)和第二个菜单的第1个内核。这样一来,大概就可以完美地正确了。
-其他的一些小细节
file指针的生命周期
在数据结构中存储file指针至关重要。但仔细想一想,file指针的生命周期似乎长到过分:从sys_mmap被调用,一直到usertrap处理缺页中断,最后到munmap释放,我们要求file指针的值需要保持稳定不变。
-这么长的生命周期,它真的可以做到吗?毕竟file指针归根到底只是一个局部变量,在syscall mmap结束之后,它还有效吗?答案是有效的,这个有效性由
-mmap
实现中对ref的增加来实现保障。在用户态中关闭一个文件,需要使用syscall
-close(int fd)
。不妨来看看close
的代码。- -
// in kernel/sysfile.c
uint64
sys_close(void)
{
int fd;
struct file *f;
if(argfd(0, &fd, &f) < 0)
return -1;
// 一个进程打开的文件都会放入一个以fd为index的文件表里,
// 在xv6中,这个文件表便是`myproc()->ofile`。
// 可以看到,关闭一个文件首先需要把它移出文件表
myproc()->ofile[fd] = 0;
// 对file指针关闭的主要操作
fileclose(f);
return 0;
}
// in kernel/file.c
// Close file f. (Decrement ref count, close when reaches 0.)
void
fileclose(struct file *f)
{
struct file ff;
acquire(&ftable.lock);
// 若ref数<0,就会直接return
if(--f->ref > 0){
release(&ftable.lock);
return;
}
// 释放file
// close不会显式地释放file指针,只会释放file指针所指向的文件,让file指针失效。
ff = *f;
f->ref = 0;
f->type = FD_NONE;
release(&ftable.lock);
if(ff.type == FD_PIPE){
pipeclose(ff.pipe, ff.writable);
} else if(ff.type == FD_INODE || ff.type == FD_DEVICE){
begin_op();
iput(ff.ip);
end_op();
}
}可以看到,当ref数>1时,file指针就不会失效。
-这就是为什么我们还需要在mmap中让file的ref数++。
-缺页中断蕴含的设计思想
如果只存入file指针,用户态要如何对对应的文件进行读写呢?
-我们可以自然想到也许需要设计一个函数,让用户在想要对这块内存读写的时候调用这个函数即可。但是,这样的方法使得用户对内存不能自然地读写,还需要使用我们新设计的这个函数,这显然十分地不美观。所以,我们需要找到一个方法,让上层的用户可以统一地读取任何的内存块,包括memory-mapped file内存块,而隐藏memory-mapped file与其他内存块读写方式不同的这些复杂细节。经历过前面几次实验的你看到这里一定能想到,有一个更加优美更加符合设计规范的方法,那就是:缺页中断!
+在修改完
+grub
文件之后,我们需要执行sudo update-grub
,来重新生成grub.cfg
文件供下次启动使用。在GRUB界面直接修改
+
我们可以在GRUB界面选中所需内核,按下e键:
++
然后就可以对启动参数进行修改,^X退出。
+值得注意的是,此修改仅对本次启动有效。如果需要长期修改,建议还是通过第一种方法去修改。
+initramfs
GRUB程序会通过
initrd.img
启动initramfs,从而进行真正的根文件系统挂载。--没做这个实验之前就知道mmap需要借助缺页中断来实现了,但实际自己的第一印象是觉得并不需要缺页中断,直到分析到这里才恍然大悟。
-“让上层的用户可以统一地读取任何的内存块,而隐藏不同类型的内存块读写方式不同的这些复杂细节”
-仔细想想,前面几个关于缺页中断的实验,比如说cow fork,lazy allocation,事实上都是基于这个思想。它们并不是不能与缺页中断分离,只是有了缺页中断,它们的实现更加简洁,更加优美。
-再次感慨os的博大精深。小小一个缺页中断,原理那么简单,居然集中了这么多设计思想,不禁叹服。
+initrd.img是一个Linux系统中的初始化内存盘(initial RAM disk)的映像文件。它是一个压缩的文件系统映像,通常在引导过程中加载到内存中,并提供了一种临时的根文件系统,以便在正式的根文件系统(通常位于硬盘上)可用之前提供必要的功能和模块。
正确答案的munmap中如果遇到未映射的页怎么办
在正确答案的munmap中:
-- -
//释放已经申请的页表项、内存,并且看看是不是需要写回
while(start_va < bounder){
if(p->filemaps[i].flags == MAP_SHARED){
//写回
filewrite(p->filemaps[i].file,start_va,PGSIZE);
}
uvmunmap(p->pagetable,start_va,1,1);
start_va += PGSIZE;
}如果map类型为
-MAP_SHARED
,并且该页尚未映射,会怎么样呢?追踪filewrite的路径
-- -
// in file.c
begin_op();
ilock(f->ip);
if ((r = writei(f->ip, 1, addr + i, f->off, n1)) > 0)
f->off += r;
iunlock(f->ip);
end_op();
// in fs.c
if(either_copyin(bp->data + (off % BSIZE), user_src, src, m) == -1) {
brelse(bp);
break;
}
log_write(bp);
// in vm.c copyin()
int
copyin(pagetable_t pagetable, char *dst, uint64 srcva, uint64 len)
{
uint64 n, va0, pa0;
while(len > 0){
va0 = PGROUNDDOWN(srcva);
pa0 = walkaddr(pagetable, va0);
if(pa0 == 0)
return -1;
// ...-
copyin
最终会在if(pa0 == 0) return -1;
这里终结,但writei
并不会在接收到-1的时候爆出panic或者是引发缺页中断,而只会把它当做文件结尾,默默地返回。并且,在
-munmap
中是一页一页地释放,而不是直接传参length全部释放,这一点也很重要。因为我们的lazy allocation很可能导致va~va+length
这一区间内只是部分页被映射,部分页没有。如果直接传参length释放,那么在遇到第一页未被映射的时候,filewrite
就会终止,该页之后的页就没有被写回文件的机会了。所以结论是,在正确实现的
-munmap
中遇到未映射的页会自动跳过,什么也不会发生。代码
数据结构
- -
// in param.h
// in proc.h
struct filemap{
uint isused;//对象池思想。该filemap是否正在被使用
uint64 va;//该文件的起始内存页地址
uint64 okva;//该文件的起始未被读入部分对应的内存地址
struct file* file;//文件
size_t length;//需要映射到内存的长度
int flags;//MAP_SHARED OR MAP_PRIVATE
int prot;//PROT_READ OR PROT_WRITE
uint64 offset;//va相对于file开头的offset
};
// Per-process state
struct proc {
struct filemap filemaps[NFILEMAP];
};mmap
具体系统调用注册过程略。
-- -
// in sysproc.c
uint64
sys_mmap(void){
uint64 addr;
int length,prot,flags,offset;
struct file* file;
if(argaddr(0,&addr) < 0 || argint(1,&length) < 0 || argint(2,&prot) < 0 || argint(3,&flags) < 0 || argfd(4,0,&file) ||argint(5,&offset) < 0)
return -1;
return (uint64)mmap((void*)addr,(size_t)length,prot,flags,file,(uint)offset);
}- -
// 映射file从offset开始长度为length的内容到内存中,返回内存中的文件内容起始地址
void* mmap(void* address,size_t length,int prot,int flags,struct file* file,uint64 offset){
// mmap的prot权限必须与file的权限对应,不能file只读但是mmap却可写且shared
if((prot&PROT_WRITE) != 0&&flags == MAP_SHARED &&file->writable == 0)
return (void*)ERRORADDR;
struct proc* p = myproc();
uint64 va = 0;
int i=0;
//找到filemap池中第一个空闲的filemap
for(i=0;i<NFILEMAP;i++){
if(!p->filemaps[i].isused){
// 获取va,也即真正的address
va = p->sz;
p->sz += length;
// 其实这里用一个memcpy会更加优雅,可惜我忘记了()
p->filemaps[i].isused = 1;
p->filemaps[i].va = va;
p->filemaps[i].okva = va;
p->filemaps[i].length = length;
p->filemaps[i].prot = prot;
p->filemaps[i].flags = flags;
p->filemaps[i].file = file;
p->filemaps[i].file->off = offset;
p->filemaps[i].offset = offset;
// 增加文件引用数
filedup(file);
break;
}
}
if(va == 0) return (void*)ERRORADDR;
// return start of free memory
uint64 start_va = PGROUNDUP(va);
// 先读入处于proc已申请的内存页区域(也即没有内存对齐情况下)
uint64 off = start_va - va;
if(off < PGSIZE){
fileread(file,va,off);
file->off += off;
p->filemaps[i].okva = va+off;
}
return (void*)va;
}usertrap
错的
- -
} else if(r_scause() == 13 || r_scause() == 15){
uint64 va = r_stval();
for(int i=0;i<NFILEMAP;i++){
// 找到va对应的filemap
if(p->filemaps[i].isused&&va>=p->filemaps[i].va
&& va<p->filemaps[i].va+p->filemaps[i].length){
// 说明本来就不应该写
if(r_scause() == 15 && ((p->filemaps[i].prot)&PROT_WRITE) == 0){
p->killed = 1;
break;
}
//说明地址不在文件范围内
if(p->filemaps[i].va+p->filemaps[i].file->ip->size <= va){
p->killed = 1;
break;
}
// 能进到这里来的都是产生了缺页中断,也就是说va对应文件数据不存在
// 我们需要维护一个okva,表示从filemaps.va到okva这段地址已经加载了文件
// 这样一来,我们这里就只需加载okva~va地址对应的文件了
// file结构体自带的off成员会由于fileread而自动增长到对应位置,所以文件可以自然地读写
uint64 start_va = p->filemaps[i].okva;// okva一定是page-align的
// 加载文件内容
while(start_va <= va){
char* mem = kalloc();
if(mem == 0){
p->killed = 1;
break;
}
memset(mem, 0, PGSIZE);
int flag = PTE_X|PTE_R|PTE_U;
if(((p->filemaps[i].prot)&PROT_WRITE) != 0){
flag |= PTE_W;
}
if(mappages(p->pagetable, start_va, PGSIZE, (uint64)mem, flag) != 0){
p->killed = 1;
kfree(mem);
break;
}
// 读入文件内容
fileread(p->filemaps[i].file,start_va,PGSIZE);
start_va += PGSIZE;
}
p->filemaps[i].okva = start_va;
break;
}
}
}对的
- -
} else if(r_scause() == 13 || r_scause() == 15){
uint64 va = r_stval();
for(int i=0;i<NFILEMAP;i++){
if(p->filemaps[i].isused&&va>=p->filemaps[i].va && va<p->filemaps[i].va+p->filemaps[i].length){
if(r_scause() == 15 && ((p->filemaps[i].prot)&PROT_WRITE) == 0){
// 说明本来就不应该写
p->killed = 1;
break;
}
if(p->filemaps[i].va+p->filemaps[i].file->ip->size <= va){
//说明地址不在文件范围内
p->killed = 1;
break;
}
uint64 start_va = PGROUNDDOWN(va);
char* mem = kalloc();
if(mem == 0){
p->killed = 1;
break;
}
memset(mem, 0, PGSIZE);
int flag = PTE_X|PTE_R|PTE_U;
if(((p->filemaps[i].prot)&PROT_WRITE) != 0){
flag |= PTE_W;
}
if(mappages(p->pagetable, start_va, PGSIZE, (uint64)mem, flag) != 0){
p->killed = 1;
kfree(mem);
break;
}
readi(p->filemaps[i].file->ip,0,(uint64)mem,va-p->filemaps[i].va+p->filemaps[i].offset,PGSIZE);
break;
}
}
}munmap
错的
- -
uint64 min(uint64 a,uint64 b){return a>b?b:a;}
// 释放文件映射以address为起始地址,length为长度这个范围内的内存地址空间
int munmap(void* address,size_t length){
struct proc* p = myproc();
uint64 va = (uint64)address;
// 找到对应的filemap
for(int i=0;i<NFILEMAP;i++){
if(p->filemaps[i].isused&&p->filemaps[i].va<=va&&p->filemaps[i].va+length>va){
// 开始释放的内存地址
uint64 start_va;
if(va == p->filemaps[i].va)
start_va = PGROUNDUP(p->filemaps[i].va);
else
start_va = PGROUNDDOWN(va);
// 结束释放的内存地址
uint64 bounder = p->filemaps[i].va + min(p->filemaps[i].file->ip->size,length);
//file的off在trap中用于表示文件已加载的位置
//在这里需要用off进行filewrite,所以需要对原本在usertrap用于记录加载位置的off进行手动保存
uint64 tmp_off = p->filemaps[i].file->off;
p->filemaps[i].file->off = p->filemaps[i].offset+va-p->filemaps[i].va;
//释放已经申请的页表项、内存,并且看看是不是需要写回
while(start_va < bounder && start_va < p->filemaps[i].okva){
if(p->filemaps[i].flags == MAP_SHARED){
//写回
filewrite(p->filemaps[i].file,start_va,PGSIZE);
}
uvmunmap(p->pagetable,start_va,1,1);
start_va += PGSIZE;
}
//修改filemap结构体的起始地址va和长度,offset也要变,因为他记录va对应的是文件哪个位置
if(va == p->filemaps[i].va){
//释放的是头几页
p->filemaps[i].offset += length;
p->filemaps[i].va = va+length;
p->filemaps[i].length -= length;
}else {
//释放的是尾几页
p->filemaps[i].length -= p->filemaps[i].length - va;
}
p->filemaps[i].file->off = tmp_off;
// 检验map的合理性
if(p->filemaps[i].length == 0 || p->filemaps[i].va >= p->filemaps[i].va+length
|| p->filemaps[i].file->off > p->filemaps[i].file->ip->size){
p->filemaps[i].isused = 0;//释放
// 注意!!!!这句话对我的错误代码来说非常重要
p->filemaps[i].file->off = 0;
fileclose(p->filemaps[i].file);
}
}
}
return 0;
}对的
+
uint64 min(uint64 a,uint64 b){return a>b?b:a;}
int munmap(void* address,size_t length){
struct proc* p = myproc();
uint64 va = (uint64)address;
for(int i=0;i<NFILEMAP;i++){
if(p->filemaps[i].isused&&p->filemaps[i].va<=va&&p->filemaps[i].va+length>va){
uint64 start_va;
if(va == p->filemaps[i].va)
start_va = PGROUNDUP(p->filemaps[i].va);
else
start_va = PGROUNDDOWN(va);
uint64 bounder = p->filemaps[i].va + min(p->filemaps[i].file->ip->size,length);
//在这里需要用off进行读写,所以需要对原本的加载处off手动保存
uint64 tmp_off = p->filemaps[i].file->off;
p->filemaps[i].file->off = p->filemaps[i].offset+va-p->filemaps[i].va;
//释放已经申请的页表项、内存,并且看看是不是需要写回
while(start_va < bounder){
if(p->filemaps[i].flags == MAP_SHARED){
//写回
filewrite(p->filemaps[i].file,start_va,PGSIZE);
}
uvmunmap(p->pagetable,start_va,1,1);
start_va += PGSIZE;
}
//修改filemap结构体的起始地址va和长度,offset也要变,因为他记录va对应的是文件哪个位置
if(va == p->filemaps[i].va){
//释放的是头几页
p->filemaps[i].offset += length;
p->filemaps[i].va = va+length;
p->filemaps[i].length -= length;
}else {
//释放的是尾几页
p->filemaps[i].length -= p->filemaps[i].length - va;
}
// 检验map的合理性
if(p->filemaps[i].length == 0 || p->filemaps[i].va >= p->filemaps[i].va+length
|| p->filemaps[i].file->off > p->filemaps[i].file->ip->size){
p->filemaps[i].isused = 0;//释放
fileclose(p->filemaps[i].file);
}
p->filemaps[i].file->off = tmp_off;
}
}
return 0;
}我们可以通过
+unmkinitramfs /boot/initrd.img-6.4.0-rc3+ /tmp/initrd/
命令解压initrd,探究里面到底有什么玩意。-
├── bin -> usr/bin
├── conf
├── etc
├── init
├── lib -> usr/lib
├── lib32 -> usr/lib32
├── lib64 -> usr/lib64
├── libx32 -> usr/libx32
├── run
├── sbin -> usr/sbin
├── scripts
├── usr
└── var
initexit和fork
exit
+
// 关闭map-file
for(int i=0;i<NFILEMAP;i++){
if(p->filemaps[i].isused){
munmap((void*)(p->filemaps[i].va),p->filemaps[i].length);
}
}可以看到,这实际上就是一个小型的文件系统,也即initramfs。它有自己的built-in Shell(BusyBox):
++
有一些较少的Shell命令(bin和sbin目录下),以及用来挂载真正的根文件系统的代码逻辑(存储在scripts目录下)。【我猜】在正常情况下,系统会执行scripts下的脚本代码挂载真正的文件系统。当挂载出现异常时,系统就会将控制权交给initramfs内置的Shell BusyBox,由用户自己探究出了什么问题。
+我们接下来可以追踪下initramfs的script目录下的文件系统挂载流程。
+挂载真正文件系统的主要函数为
+local_mount_root
:-
仅展示主要流程代码
local_mount_root()
{
预处理,获取参数等(也即上面grub.cfg配置的root=UUID)
local_top
if [ -z "${ROOT}" ]; then
panic "No root device specified. Boot arguments must include a root= parameter."
fi
根据UUID获取对应的块设备
local_device_setup "${ROOT}" "root file system"
ROOT="${DEV}"
挂载前的预处理
local_premount
挂载
mount ${roflag} ${FSTYPE:+-t "${FSTYPE}"} ${ROOTFLAGS} "${ROOT}" "${rootmnt?}"
}fork
+
for(int i=0;i<NFILEMAP;i++){
np->filemaps[i].isused = p->filemaps[i].isused;
np->filemaps[i].va = p->filemaps[i].va;
np->filemaps[i].okva = p->filemaps[i].okva;
np->filemaps[i].file = p->filemaps[i].file;
np->filemaps[i].length = p->filemaps[i].length;
np->filemaps[i].flags = p->filemaps[i].flags;
np->filemaps[i].offset = p->filemaps[i].offset;
np->filemaps[i].prot = p->filemaps[i].prot;
if(np->filemaps[i].file)
filedup(np->filemaps[i].file);
}由于研究这个是错误驱动(乐),因而我只主要看了下
+local_device_setup
:-
$1=device ID to mount设备ID
$2=optionname (for root and etc)要挂载的是什么玩意,此处应为root file system
$3=panic if device is missing (true or false, default: true)
Sets $DEV to the resolved device node $DEV是最终获取到的块设备
local_device_setup()
{
local dev_id="$1"
local name="$2"
local may_panic="${3:-true}"
local real_dev
local time_elapsed
local count
获取grub.cfg的rootdelay参数的设备等待时间。如果没有该参数,默认是30秒
local slumber=30
if [ "${ROOTDELAY:-0}" -gt $slumber ]; then
slumber=$ROOTDELAY
fi
等待设备
case "$dev_id" in
UUID=*|LABEL=*|PARTUUID=*|/dev/*)
FSTYPE=$( wait-for-root "$dev_id" "$slumber" )
;;
*)
wait_for_udev 10
;;
esac
等待结束了。如果条件为真,说明还是获取不到对应的设备,那就只能说明这个设备死了
所以我们就得把问题告诉用户,让用户自己解决,并且进入BusyBox Shell
We've given up, but we'll let the user fix matters if they can
while ! real_dev=$(resolve_device "${dev_id}") ||
! get_fstype "${real_dev}" >/dev/null; do
if ! $may_panic; then
echo "Gave up waiting for ${name}"
return 1
fi
echo "Gave up waiting for ${name} device. Common problems:"
echo " - Boot args (cat /proc/cmdline)"
echo " - Check rootdelay= (did the system wait long enough?)"
if [ "${name}" = root ]; then
echo " - Check root= (did the system wait for the right device?)"
fi
echo " - Missing modules (cat /proc/modules; ls /dev)"
panic "ALERT! ${dev_id} does not exist. Dropping to a shell!"
done
DEV="${real_dev}"
}修改uvmcopy和uvmunmap
-]]>
// in uvmunmap()
if((*pte & PTE_V) == 0){
*pte = 0;
continue;
}
// in uvmcopy()
if((*pte & PTE_V) == 0)
//panic("uvmcopy: page not present");
continue;
防火墙
-sudo ufw status numbered # 查看 |
可以看到,这里如果进入错误状态,最终就是这样的效果2333:
+http://localhost/webdemo4_war/*.do
。
idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/
typora 替换图片asset
-\!\[.*\]\(D:\\aWorkStorage\\hexo\\blog\\source\\_posts\\阅读JDK容器部分源码的心得体会2【Map部分】\\(.*)\.png\)
,
替换结果{% asset_img $1.png %}
--A map cannot contain duplicate keys; each key can map to at most one value.
-This interface takes the place of the Dictionary class.
-The Map interface provides three collection views, which allow a map’s contents to be viewed as a set of keys, collection of values, or set of key-value mappings.
-The order of a map is defined as the order in which the iterators on the map’s collection views return their elements. map的元素顺序取决于集合元素顺序的意思?
-Note: great care must be exercised if mutable objects are used as map keys. The behavior of a map is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is a key in the map. 【这个跟set的那个是一样的】
-
map没有迭代器
-public interface Map<K,V> { |
get return null when value==NULL or key不存在。
-为了区分这两种情况,写代码时可以用:
-if !containsKey(key){ |
其实源码中的getordefault方法就给出了应用典范
-default V getOrDefault(Object key, V defaultValue) { |
--//Returns a Set view of the keys contained in this map.
-//The set is backed by the map, so changes to the map are reflected in the set, and vice-versa.
-
如下代码测试:
-public static void main(String[] args) { |
可得,与之前的List一样,这个view都是纯粹基于原数组的,实时变化的。
-在应用中可发现,可以通过map的key和value的set来对map进行遍历。
----
//The map can be modified while an iteration over the set is in progress
//when using the setValue operation on a map entry returned by the iterator
//or through the iterator's own remove operation
相比于其它的view,多了第二句话
---If the function returns null no mapping is recorded.
-If the function itself throws an (unchecked) exception, the exception is rethrown, and no mapping is recorded.
-The most common usage is to construct a new object serving as an initial mapped value or memoized result, as in:
-- -
map.computeIfAbsent(key, k -> new Value(f(k)));Or to implement a multi-value map, Map<K,Collection
->, supporting multiple values per key: +
map.computeIfAbsent(key, k -> new HashSet<V>()).add(v);此为《信息存储与管理(第二版):数字信息的存储、管理和保护》的看书总结,相当于是对存储技术的一个简单的名词入门。
它这说的还是很抽象的,下面给出一个使用computeIfAbsent的优雅实例:
-TreeMap()) Treemap With Object
+本章节我印象最深的还是以前就不大了解的DNS,今天看到书的描写真有种豁然开朗的感觉。
+DNS服务器用于保存域名—IP地址的映射对,为了增加查找效率,DNS根据域名的分级采用树形组织,例如hitsz.edu.cn/
可以相当于是/cn/edu/hitsz
,包含了/
、cn
、edu
这几个域。根DNS服务器存储着根域,记录了所有一级域名对应DNS服务器的IP地址。所有的DNS服务器都会保存根服务器的IP地址。
--+
computeIfAbsent
returns the value that is already present in the map, or otherwise evaluates the lambda and puts that into the map, and returns that value.世界上只有13个根DNS服务器IP地址,但是有很多台根DNS服务器。
var line = "line"; |
computIfAbsent发现此时map里面没有这个“line”key,就执行第二个参数的lambda表达式,把一个new TreeMap<>以line为关键字放入,并且返回该TreeMap。
+主机需要手动配置DNS服务器地址。
+当浏览器需要填写请求头时,它需要通过系统调用向操作系统发送DNS查询请求。操作系统将DNS请求发送给配置在主机上的DNS服务器(下称A),A再向根DNS服务器发送请求。根DNS服务器解析域名,返回下一级DNS服务器的IP地址。A再向下级DNS服务器再次发送请求,下级再返回下下级IP地址。以此类推,最终A就能得到目标IP地址的正确响应。整个过程如下图所示:
+与此同时,各个DNS服务器都会有定时刷新的缓存,从而加速了查找效率。
+本章前面大多讨论TCP/IP具体协议内容,以前已经了解过很多次了就不多赘述。所以TCP/IP部分就以分点的形式随意列举一下:
+IP 中还包括 ICMPA 协议和 ARPB 协议。ICMP 用于告知网络包传送过程中产生的错误以及各种控制消息,ARP 用于根据 IP 地址查询相应的以太网 MAC 地址。
-+看起来非常实用:
-This method may be of use when combining multiple mapped values for a key【相同key不同value合并】. For example, to either create or append a String msg to a value mapping:
-- -
map.merge(key, msg, String::concat)所举代码段意为把新值通过字符串拼接接在旧值后面。
-应该也可以用于集合合并。总之具体实现方法取决于传入的function参数,非常实用
-
套接字中记录了用于控制通信操作的各种控制信息,协议栈则需要根据这些信息判断下一步的行动,【包括应用程序信息和协议栈状态信息】这就是套接字的作用。所以需要针对不同协议栈实现不同的socket。
map本身没有迭代器。
-因而在对map进行遍历时,只能通过其keyset、valueset以及entryset来实现。
-具体详见:HashMap的四种遍历方式
+是的,回想当初CS144,也是socket来负责有特定消息时调用TCP相关函数来通知处理。
连接 connect
+连接实际上是通信双方交换控制信息,在套接字中记录这些必要信息并准备数据收发的一连串操作。
+--To implement an unmodifiable map, the programmer needs only to extend this class and provide an implementation for the entrySet method, which returns a set-view of the map’s mappings. Typically, the returned set will, in turn, be implemented atop AbstractSet. This set should not support the add or remove methods, and its iterator should not support the remove method.
-To implement a modifiable map, the programmer must additionally override this class’s put method (which otherwise throws an UnsupportedOperationException), and the iterator returned by entrySet().iterator() must additionally implement its remove method.
-
最核心的还是entrySet。其余所有的方法,都是通过enrtSet实现的。而给定了enrty这个数据结构的实现方式,剩下的就是entrySet具体怎么实现了。AbstractMap把entrySet的实现抽象了出来,交给了其实现类去具体实现。
-public abstract class AbstractMap<K,V> implements Map<K,V> { |
--Each of these fields are initialized to contain an instance of the appropriate view the first time this view is requested. The views are stateless, so there’s no reason to create more than one of each.
-
不同于之前List的sublist和sorted set的subset,它俩是调用创建view方法时才构造出一个新的对象,map是直接把values和keys视图放入成员变量了,因为Collection的视图从实用角度来说有起始和终点更实用,map不需要这个性质,因此作为成员变量花费更小
哈希表+链表/红黑树
---permits null values and the null key允许空,其hash应该是0
-The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.不同步
-This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.无序
-这应该差不多就是个桶链表
-An instance of HashMap has two parameters that affect its performance: initial capacity and load factor.
-The capacity is the number of buckets in the hash table.桶数量=capacity
-The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. 如果装载百分比达到load factor,hashmap的capacity就会自动增长。
-When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed.如果元素数量>=load factor*capacity,就会自动增长并且重新hash。
-默认的load factor是0.75.【我其实觉得这个数很有意思。它是二进制意义上的整除数,因而计算应该很方便:它可以被整整表示,并且计算时可以拆成“2^-1+2^-2”以供移位简化】
-我们设置capacity和load factor的意图应该是要尽量减少rehash的次数。
-Note that using many keys with the same hashCode() is a sure way to slow down performance of any hash table使用多个相同的key【指hashcode相同】会降低性能【?】
-https://stackoverflow.com/questions/43911369/hashmap-java-8-implementation等会看看
-
总之意思差不多就是,hashmap的数据结构:
-table数组,每个成员都是一个桶,桶里面装着结点。table默认长度为16
-每个桶内结点的结构依具体情况(该桶内元素多少)来决定,桶内元素多则用树状结构,少就用简单的线性表结构。线性结构为Node<K,V>,树状结构为TreeNode<K,V>。
-当一个线性表桶内结点多于临界值,就需要进行树化,会从链表变成红黑树;当整个hashmap结点数多于临界值,就需要增长capacity并且进行rehash。
-hashmap的桶的装配:首先通过key的hashcode算出一个hash值,然后再把该hash值与n-1相与就能得到桶编号。接下来再在桶内找到应插入的结点就行。
-public class HashMap<K,V> extends AbstractMap<K,V> |
hash=原hashcode^(原hashcode逻辑右移16位)
-这样的话,由于右移16位补零,此时高位的所有比特位都跟原来一样,低位的比特位变成了融合高低位特点的东西,这样就可以减少冲突,增加均匀性
+动态调整等待时间
+具体看这个视频,讲得非常不错
- -假设table此时为默认长度16.则n-1=15
-写出15的二进制形式:0000 1111,可以发现,任何数跟它相与,结果都一定为0000 xxxx,永不越界。
-写出16的二进制形式:0001 0000,可以发现,任何数跟它相与,结果都一定为16或者0.
-可以发现15有非常好的性质。
-而扩展出来,任何2的幂次方-1都具有这样的良好的性质。**这也是为什么hashmap要求表的长度应该为2的幂次。**
-而且,除了不会越界,还有一点就是,这个任何数与15相与的与操作就相当于,任何数对16取余的取余操作。这点实在是佩服啊,把复杂的取余操作在该场景下直接用一个位运算就搞定了。
+以太网的定义
+树状结构时结点的默认排序方式是by hashCode。但如果两个结点元素之间是同一个class C,并且这个C实现了Comparable方法,那么就不会按照它们的hashCode比较,而是会调用class C的compareTo方法。
-(We conservatively(保守地) check generic types via reflection to validate(证实) this – see method comparableClassFor).
-也就是说这个comparableClassFor方法的意图就是,如果这个类是comparable的,就返回它具体类型,如果不是返回null。
+系统初始化时MAC地址的设置
+MAC地址是上电后由驱动程序从ROM中读取的,而非自动获取的
不同于AbstractMap中entrySet的核心作用,HashMap的put、get、clear等等等核心函数都不依赖于entrySet了,毕竟结构改变得比较多了。因而这里的entrySet字段保留,只是为了呼应AbstractMap中keyset和valueset的实现,以及补充AbstractMap中未给出的EntrySet实现。
+电信号转换【这个帅得不行】
+为了区分连续的1或0,我们就需要同时发送数据信号和时钟信号,然而这样开销太大,因而我们引入了上升沿。
+上升沿本质上是数据信号和时钟信号叠加而成的结果,叠加方式是异或。
+提到异或是否感觉豁然开朗?是的,这东西恢复时也是使用了异或的性质:接收方从帧头获取时钟频率从而得到时钟信号,跟收到的叠加信号进行再次叠加(异或),就可以获得原来的数据信号了。
+我只能说牛逼,一直以来对异或的视角还停留在单纯的数字,这个波形的物理概念真的惊到我了。
+实例:
+此时需要复制oldTab中的所有结点。但注意,由于此时发生了扩容,hash的计算发生了变化,因而不能全部照搬不动oldTab中的下标,否则产生错误。因而我们需要了解一下如何调整下标。
-首先由代码可得,对于oldTab!=NULL的情况下,newCap一定是扩为原来的两倍的。因而以下只需讨论扩容为两倍的情况。
-由第2点可知,假设现在容量为16,扩容为原来的两倍,则hash掩码应该为0000 1111,扩容后,hash掩码应该为0001 1111,可见就只是多了一位,因而,oldTab中,若这一位的值为0,则在新表和旧表中位置的下标应该是一样的;若这一位的值为1,则新表下标=旧表下标+offset,offset正是等于0001 0000.而这个“0001 0000”,正是oldCap!
-对于容量为其他值,全部道理都是一样的。
-因而我们要做的,是对旧表的每一个桶内的所有结点,把它们分成两类,一类为(e.hash & oldCap) == 0【也就是这一位值为0 情况】和(e.hash & oldCap) == 1,然后对这两类进行在新表中分别映射即可。这段代码便做了这样的事。
-//5 |
这篇文章也写得很好:
- +半双工模式【同一时刻只能进行发or收】使用集线器,全双工模式【发or收可以并行】使用交换机。半双工模式需要进行载波监听碰撞检测。
注意点有二:
-①不继承Iterator接口
-②抽象,具体实现类为EntryIterator、KeyIterator和ValueIterator
-③map的接口定义是没有iterator的,因此map不能通过hashiterator迭代,只能通过其vie来实现【三个具体实现类】
+服务器的操作系统具备和路由器相同的包转发功能,当打开这一功能时,它就可以像路由器一样对包进行转发。在这种情况下,当收到不是发给自己的包的时候,就会像路由器一样执行包转发操作。
哈希表+链表/红黑树+有序队列
--Hash table and linked list implementation of the Map interface, with predictable iteration order.
-This implementation differs from HashMap in that it maintains a doubly-linked list running through all of its entries.
-This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order).有序,顺序为元素插入的顺序
-Note that insertion order is not affected if a key is re-inserted into the map. 当修改key的value值时,key的插入序不变
-此实现既让hashmap变得有序,又不会像TreeMap一样有高成本。
-It can be used to produce a copy of a map that has the same order as the original, regardless of the original map’s implementation.
-- -
这样可以保持copymap的原有顺序
-A special constructor is provided to create a linked hash map whose order of iteration is the order in which its entries were last accessed, from least-recently accessed to most-recently (access-order). This kind of map is well-suited to building LRU caches. 可以有一个排序方式,顺序为最近最少访问->最近访问,这可以用来构建LRU cache【LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。】
-至于这个“access”怎么定义:
--Invoking the put, putIfAbsent, get, getOrDefault, compute, computeIfAbsent, computeIfPresent, or merge methods results in an access to the corresponding entry (assuming it exists after the invocation completes). The replace methods only result in an access of the entry if the value is replaced. The putAll method generates one entry access for each mapping in the specified map, in the order that key-value mappings are provided by the specified map’s entry set iterator.
-注意没有remove
+]]>
idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/
typora 替换图片asset
+ \!\[.*\]\(D:\\aWorkStorage\\hexo\\blog\\source\\_posts\\阅读JDK容器部分源码的心得体会1【Collection部分】\\(.*)\.png\)
,
替换结果{% asset_img $1.png %}
也因此,对map视图【各个set】的访问不算access。【因为不调用任意一个上面方法】
-可以重写 removeEldestEntry(Map.Entry) 方法,以在将新映射添加到映射时自动删除陈旧映射的策略。
-/*实现这个接口的类可用于for-each循环*/ |
//1
-Iteration over the collection-views of a LinkedHashMap requires time proportional to the size of the map, regardless of its capacity.不同于hashmap,迭代时间与容量无关。
-In access-ordered linked hash maps, merely querying the map with get is a structural modification.注意,对于access-ordered的lhm来说,**get也是一个structural modification,因为可能会修改排序顺序**。所以迭代时只能使用Iterator的next方法来得到结点,迭代器访问不会对accessorder有影响。
-代码测试:
-LinkedHashMap<String,Integer> map = new LinkedHashMap<>(16,0.75f,true); |
+-引入迭代器的目的是为了**统一**“对容器里的元素进行遍历”这一操作。
总之意思就是,LinkedHashMap的数据结构:
-在HashMap哈希表+链表/红黑树的基础上,添加一个双端队列,该双端队列的作用是来维持内部的有序,因而开销比较大。应该只提供插入序和LRU序,其他需要用到compare的排序方法需要对某些方法(如afternodeXXX)进行重写,或者直接使用sorted map。
-LHM的一个很特殊的地方就是,它可以实现一个LRU这样的cache结构,只需要你重载removeEldestEntry return true。还可以在LHM的基础上实现有限长度map,只需要你重载removeEldestEntry 当元素>=某值时返回true。总而言之,你可以建造一个类在LHM的基础上,如果需要对map的长度有限制。
-LHM对LRU的实现是,一旦某个结点用到了,就立刻把他移到最队尾,然后每次淘汰淘汰队首。
-public class LinkedHashMap<K,V> |
public interface Iterator<E> { |
LinkedHashMap的结构跟HashMap是一样的,也就是都baked by array。此处为什么“迭代时间与容量无关”,是因为LinkedHashMap内部维护了一个简单的链表队列【包含所有元素】,迭代的时候是对这个队列进行迭代,而不是像HashMap一样通过表迭代。
-怪不得读源码时觉得有些地方明明不重写HashMap也可以它却重写了。原来是因为这个性能问题啊
--A Map that further provides a total ordering on its keys.
-The map is ordered according to the natural ordering of its keys, or by a Comparator typically provided at sorted map creation time.
-All keys inserted into a sorted map must implement the Comparable interface (or be accepted by the specified comparator).
-+
+-注意,iterator并不指向具体元素,它指向的是元素的间隙
+这些^就是iterator所指的位置。这样就能理解iterator的next和previous了吧2333
+-
关于这部分,详细见sorted set
+而set()所修改的元素,是其上一次调用next()或者previous()方法所返回的元素。
+What’s the meaning of this source code of interface Collection in JAVA?
最大的特点就是可以人为定义有序并且有sub map
-代码:
+
public interface SortedMap<K,V> extends Map<K,V> {
Comparator<? super K> comparator();
SortedMap<K,V> subMap(K fromKey, K toKey);
SortedMap<K,V> headMap(K toKey);
SortedMap<K,V> tailMap(K fromKey);
//也是默认第一个是低的最后一个是高的,就跟LHM的第一个是最少使用,最后一个是最近使用一样
//Returns the first (lowest) key currently in this map.
K firstKey();
//Returns the last (highest) key currently in this map.
K lastKey();
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
}ListIterator(I)
-
public interface ListIterator<E> extends Iterator<E> {
boolean hasNext();
E next();
void remove();
//newly added as iterator below:
boolean hasPrevious();
E previous();
//返回后续调用 next 将返回的元素的索引。[应该就是当前元素索引]
int nextIndex();
int previousIndex();
//This call can be made only if neither remove nor add have been called after the last call to next or previous.
void set(E e);
//The element is inserted immediately before the element that would be returned by next, if any, and after the element that would be returned by previous, if any.
void add(E e);
}NavigableMap
-A SortedMap extended with navigation methods returning the closest matches for given search targets.
-The performance of ascending operations and views is likely to be faster than that of descending ones.
-submap都多加了几个参数:inclusive or exclusive
-其entry不支持setValue,只能通过map自身的put方法改变value。因为要求前者只是map的快照
++-Note that the remove and set(Object) methods are not defined in terms of the cursor position; they are defined to operate on the last element returned by a call to next or previous().
跟navigable set差不多的定义
-代码:
+
public interface NavigableMap<K,V> extends SortedMap<K,V> {
Map.Entry<K,V> lowerEntry(K key);
K lowerKey(K key);
Map.Entry<K,V> floorEntry(K key);
K floorKey(K key);
Map.Entry<K,V> ceilingEntry(K key);
K ceilingKey(K key);
Map.Entry<K,V> higherEntry(K key);
K higherKey(K key);
Map.Entry<K,V> firstEntry();
Map.Entry<K,V> lastEntry();
Map.Entry<K,V> pollFirstEntry();
Map.Entry<K,V> pollLastEntry();
NavigableMap<K,V> descendingMap();
NavigableSet<K> navigableKeySet();
NavigableSet<K> descendingKeySet();
NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive,
K toKey, boolean toInclusive);
NavigableMap<K,V> headMap(K toKey, boolean inclusive);
NavigableMap<K,V> tailMap(K fromKey, boolean inclusive);
SortedMap<K,V> subMap(K fromKey, K toKey);
SortedMap<K,V> headMap(K toKey);
SortedMap<K,V> tailMap(K fromKey);
}Collection
Collection(I)
代码
-
/*
无直接实现类,一般用于需要使用多态传递参数的场合
其所有子类都必须有两个构造器: a void (no arguments) constructor, which creates an empty collection, and a constructor with a single argument of type Collection, which creates a new collection with the same elements as its argument. 这点语法上不会强制实现(因为接口不能强制构造方法),但其实是约定成俗的。
*/
public interface Collection<E> extends Iterable<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
//有关“safe”的问题讨论见下
Object[] toArray();
/*
Like the toArray() method, this method acts as bridge between array-based and
collection-based APIs. Further, this method allows precise control over the runtime
type of the output array, and may, under certain circumstances, be used to save
allocation costs.
Suppose x is a collection known to contain only strings. The following code can be
used to dump the collection into a newly allocated array of String:
String[] y = x.toArray(new String[0]);
说明还是有类型限制的
ArrayStoreException – if the runtime type of the specified array is not a supertype
of the runtime type of every element in this collection
*/
<T> T[] toArray(T[] a);
/*
给集合增加一个元素。
If a collection refuses to add a particular element for any reason other than that it
already contains the element, it must throw an exception (rather than returning
false).
*/
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
//重复了怎么办
boolean addAll(Collection<? extends E> c);
//重复的会全弄走吗
boolean removeAll(Collection<?> c);
/*
删除此集合中满足给定谓词的所有元素。
在迭代期间或由谓词引发的错误或运行时异常将转发给调用者。
@return true if any elements were removed
*/
default boolean removeIf(Predicate<? super E> filter) {
//非空filter
Objects.requireNonNull(filter);
boolean removed = false;
//迭代该集合
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
/*
把集合c中没有的元素全部移除
@return true if this collection changed as a result of the call
*/
boolean retainAll(Collection<?> c);
void clear();
boolean equals(Object o);
/*
Any class that overrides the Object.equals method must also override the
Object.hashCode method.
c1.equals(c2) 相当于 c1.hashCode()==c2.hashCode().
*/
int hashCode();
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, 0);
}
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
default Stream<E> parallelStream() {
return StreamSupport.stream(spliterator(), true);
}
}TreeMap
--NavigableMap的红黑树实现
-key不允许空,空会抛出异常
-Note that this implementation is not synchronized.
-fail-fast
-All Map.Entry pairs returned by methods in this class and its views represent snapshots of mappings at the time they were produced. They do not support the Entry.setValue method. (Note however that it is possible to change mappings in the associated map using put.)【navigable map的性质】
-具体代码就不看了
-对Collection和Map的总结
-
fail-fast
--The iterators returned by all of this class’s “collection view methods” are fail-fast: if the map is structurally modified at any time after the iterator is created, in any way except through the iterator’s own remove method, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.
-Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.
-都使用了modcount进行并发检查,都具有fail-fast的特点(关于此的详细解说,可见AbstractList第四点和List第二点),因而只允许在迭代中使用迭代器的remove方法进行结构性改变。【注意:对于LinkedHashMap中access order排序,get方法也是structural modification,因而也只能通过迭代器的next方法获取元素】
+其中,
+
-]]>- -
“Bags or multisets (unordered collections that may contain duplicate elements) should implement this interface directly.”
Set是不允许重复的元素集合的ADT,【ADT:抽象数据结构】
+Bag是元素集合的ADT,允许重复.
+通常,任何包含元素的东西都是Collection.
+任何允许重复的集合都是Bag,否则就是Set.
+通过索引访问元素的任何包都是List.
+在最后一个之后附加新元素并且具有从头部(第一索引)移除元素的方法的Bag是Queue.
+在最后一个之后附加新元素并且具有从尾部(最后一个索引)移除元素的方法的Bag是Stack.
————————————————
原文链接:https://blog.csdn.net/weixin_34239718/article/details/114036886- -
not synchronized
上面介绍到的几个类,除了Vector外,都是线程不同步的。可以用此方式让其线程同步。
-
Map m = Collections.synchronizedMap(new LinkedHashMap(...));是否允许null
除了TreeSet、TreeMap、ArrayDeque之外,都是允许空(key/value)的
+- -
“destructive” methods 和”undestructive” methods
这回答里写得很清楚:What are destructive and non-destructive methods in java?
是否有序
List都是插入序,HashSet无需,HashMap也无序(但其实算是有内部桶序的),LinkedHashMap有插入序和LRU序(依靠内部增加简单队列的消耗),TreeSet有序,TreeMap有序【这俩靠红黑树的遍历顺序(二叉搜索树嘛)】。
+- -
recursive traversal of the collection
++Some collection operations which perform recursive traversal of the collection may fail with an exception for self-referential instances where the collection directly or indirectly contains itself.
+Java 8 vs Java 7 Collection Interface: self-referential instance
+这个的第二个回答【较长的那个】写得很棒,较短的那个似乎是错误的。
+正如描述所说的,“directly or indirectly contains itself”,回答里那个例子正是因为“indirectly contains itself”。
+不仅仅是集合,每个Object都可能出现这样的错误(因为都包含有toString)
+但是注意一点:
++ +
ArrayList l1 = new ArrayList();
l1.add(l1);
System.out.println(l1.toString());
//输出:[(this Collection)]这段代码是正常的,是因为ArrayList里面toString的实现:
++ +
sb.append('[');
for (;;) {
E e = it.next();
//注意此句
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}规避了这种风险。
+下面的hashcode是不正常的,因为hashcode实现没有规避这种风险。
实现的约定接口
都Cloneable,Serializable
-ArrayList/Vector:RandomAccess
+- +
关于toArray的讨论
+ +
For toArray() :
The returned array will be "safe" in that no references to it are maintained by this collection. (In other words, this method must allocate a new array even if this collection is backed by an array). The caller is thus free to modify the returned array.+ +
List<String> list = Arrays.asList("foo", "bar", "baz");
String[] array = list.toArray(new String[0]);
array[0] = "qux";
//修改数组不会修改列表
System.out.println(list.get(0)); // still "foo"
list.set(0,"haha");
//修改列表也跟修改数组无关了
System.out.println(list.get(0)+array[0]);+ +
ArrayList<Student> a = new ArrayList<>();
a.add(new Student("Sarah",17));
Student[] s = a.toArray(new Student[0]);
//换一个引用对象
s[0]=new Student("Lily",20);
System.out.println(a.get(0)==s[0]);//false+ +
ArrayList<Student> a = new ArrayList<>();
a.add(new Student("Sarah",17));
Student[] s = a.toArray(new Student[0]);
//修改引用对象
s[0].name = "Lily";
System.out.println(a.get(0).name);//Lily这也就说明,toArray()实际上是把list的元素复制一份弄成array,直接把值粘贴进去。
+对于引用对象,list的元素实际上应该存的是对象在堆中的地址。所谓的“安全”指的是,修改array中的元素的值【也即对象地址】,也就是换一个气球牵,是不会影响原来list的元素的值的。
+因而,对于样例1和2,我们其实给array的元素换了个气球牵,或者是把list换了个气球牵,相互对象不同,没什么影响。
+对于样例3,我们修改了list和array共同指向的对象【就像C语言的指针那样】
+以上参考自What does “Safe” mean in the Collections.toArray() JavaDoc?
+关于default关键字
+starkoverflow关于为什么要设立default的讨论:
+What is the purpose of the default keyword in Java
+
Default methods were added to Java 8 primarily to support lambda expressions.
此为《程序员的自我修养:链接、装载与库》(俞甲子,石凡,潘爱民)的看书总结。
++-To implement an unmodifiable collection, the programmer needs only to extend this class and provide implementations for the iterator and size methods.
To implement a modifiable collection, the programmer must additionally[也要搞上面的] override this class’s add method (which otherwise throws an UnsupportedOperationException), and the iterator returned by the iterator method must additionally implement its remove method.
链接前的编译阶段可以生成.o文件,.o文件是ELF文件,里面含有段表、符号表、bss段、common段等链接辅助段。
-执行可执行文件时,首先要通过fork创建一个新的子进程,然后要通过exec为子进程制定可执行文件装载逻辑。在exec系统调用中,会进行elf文件的读取解析。它会解析elf header,根据其各种信息将程序copy到内存(进程的虚拟地址空间)中,后者也就是我们所研究的装载。
-装载不同于“节(section),是以”段“(segment)为单位。进程虚拟地址空间被分为很多个VMA,每个VMA都有不同的属性(如权限,可读可写可执行)。ELF可执行文件除去在链接/编译过程中被视为一个个连续的节之外,它还会在链接的时候根据每个节的属性不同重排节,把属性相同的节连续放在一起成为一个个段。链接的时候还会形成程序头表。对应关系:节——段表,段——程序头表
-装载进内存时,是以段为单位,一个段就对应着一个VMA。
-内核通过execve
系统调用装载完ELF可执行文件以后就返回到用户空间,将控制权交给程序的入口。
对于不同链接形式的ELF可执行文件,这个程序的入口是有区别的。对于静态链接的可执行文件来说,程序的入口就是ELF文件头里面的e_entry指定的入口地址;对于动态链接的可执行文件来说,如果这时候把控制权交给e_entry指定的入口地址,那么肯定是不行的,因为可执行文件所依赖的共享库还没有被装载,也没有进行动态链接。所以对于动态链接的可执行文件,内核会分析它的动态链接器地址(在“.interp”段),将动态链接器映射至进程地址空间,然后把控制权交给动态链接器。
-编译器编译源代码后生成的文件叫做目标文件(Object文件),目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。
-Linux的可执行文件遵从ELF的结构模式。
-ELF可以分为这几类:
-也即.o(可重定位文件)、.exe(无后缀)(可执行文件)、.so(动态链接库)、.a(静态链接库)、core dump。
-目标文件中的内容至少有编译后的机器指令代码、数据。没错,除了这些内容以外,目标文件中还包括了链接时所须要的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以“节”(Section)的形式存储,有时候也叫“段”(Segment)。
-众所周知,强大而又复杂的C++拥有类、继承、虚机制、重载、名称空间等这些特性,它们使得符号管理更为复杂。最简单的例子,两个相同名字的函数func(int)和func(double),尽管函数名相同,但是参数列表不同,这是C++里面函数重载的最简单的一种情况,那么编译器和链接器在链接过程中如何区分这两个函数呢?为了支持C++这些复杂的特性,⼈们发明了符号修饰(Name Decoration)或符号改编(Name Mangling)的机制。
-C++为了与C兼容,在符号的管理上,C++有一个用来声明或定义一个C的符号的“extern “C””关键字用法:
-extern ”C” { |
public abstract class AbstractCollection<E> implements Collection<E> { |
C++编译器会将在extern “C” 的大括号内部的代码当作C语言代码处理。所以很明显,上面的代码中,C++的名称修饰机制将不会起作用。
-多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将会出现符号重复定义的错误。这种符号的定义可以被称为强符号(Strong Symbol)。有些符号的定义可以被称为弱符号(Weak Symbol)。对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。我们也可以通过GCC的__attribute__((weak))
来定义任何一个强符号为弱符号。
目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们须要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用(Strong Reference)。与之相对应还有一种弱引用(Weak Reference),在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器对于该引用不报错。
-链接器处理强引用和弱引用的过程几乎一样,只是对于未定义的弱引用,链接器不认为它是一个错误。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值,以便于程序代码能够识别。
-__attribute__ ((weakref)) void foo(); |
其中:
+what’s the usage of the code in the implementation of AbstractCollection’s toArray Method
+Yes, you're right, as the javadoc sais, this method is prepared to return correctlly even if the Collection has been modified in the mean time.【并发安全】 That's why the initial size is just a hint. The usage of the iterator also ensures avoidance from the "concurrent modification" exception. |
+-
这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数。
---这里很帅,有条件编译那味了
+The Queue interface does not define the blocking queue methods, which are common in concurrent programming. These methods, which wait for elements to appear or for space to become available, are defined in the java.util.concurrent.BlockingQueue interface, which extends this interface.
+Queue implementations generally do not define element-based versions of methods equals and hashCode【就是不会像之前的list一样遍历一遍通过单个元素的hashcode计算整体的hashcode】 but instead inherit the identity based versions from class Object【hashcode由对象决定】, because element-based equality is not always well-defined for queues with the same elements but different ordering properties.
+Each of these methods exists in two forms: one throws an exception if the operation fails, the other returns a special value (either null or false, depending on the operation). 容量受限的队列推荐使用第二种form
在Linux程序的设计中,如果一个程序被设计成可以支持单线程或多线程的模式,就可以通过弱引用的方法来判断当前的程序是链接到了单线程的Glibc库还是多线程的Glibc库(是否在编译时有-lpthread选项),从而执行单线程版本的程序或多线程版本的程序。我们可以在程序中定义一个pthread_create函数的弱引用,然后程序在运行时动态判断是否链接到pthread库从而决定执行多线程版本还是单线程版本:
-+
int pthread_create(pthread_t*, const pthread_attr_t*, void* (*)(void*),
void*) __attribute__ ((weak));
int main()
{
if(pthread_create) {
printf("This is multi-thread
version!\n");
// run the multi-thread version
// main_multi_thread()
} else {
printf("This is single-thread
version!\n");
// run the single-thread version
// main_single_thread()
}
}代码:
+ +
public interface Queue<E> extends Collection<E> {
/*
The offer method inserts an element if possible, otherwise returning false.
不同于Collection的add方法,offer添加失败时不会抛出异常,而是直接return false
*/
boolean add(E e);
boolean offer(E e);
/*
The remove() and poll() methods differ only in their behavior
when the queue is empty:
the remove() method throws an exception, while the poll() method returns null.
*/
E remove();
E poll();
/*
The element() and peek() methods return,
but do not remove, the head of the queue.
*/
E element();
E peek();
}Deque(I)
++双端队列。
+The name deque is short for “double ended queue” and is usually pronounced “deck”.
++ +
This interface provides two methods to remove interior elements, removeFirstOccurrence and removeLastOccurrence.
++ +
+ -
--弱符号大概是说该变量可以被定义多次,最终链接时再进行决议;弱引用大概是说该变量(函数)可以不被定义。
静态链接
静态链接库(.a文件)本质上是一堆.o文件的集合。静态链接的基本过程:
-+
test.c ——(compile)——>test.o——(link)——>test(ELF exe)
↑
lib.a代码:
-
public interface Deque<E> extends Queue<E> {
void addFirst(E e);
void addLast(E e);
boolean offerFirst(E e);
boolean offerLast(E e);
E removeFirst();
E removeLast();
E pollFirst();
E pollLast();
E getFirst();
E getLast();
E peekFirst();
E peekLast();
boolean removeFirstOccurrence(Object o);
boolean removeLastOccurrence(Object o);
// *** Queue methods ***
boolean add(E e);
boolean offer(E e);
E remove();
E poll();
E element();
E peek();
// *** Stack methods ***
void push(E e);
E pop();
// *** Collection methods ***
boolean remove(Object o);
boolean contains(Object o);
public int size();
Iterator<E> iterator();
//Returns an iterator over the elements in this deque in reverse sequential order.
Iterator<E> descendingIterator();
}静态链接其实也就是分为两大步骤:
--
空间与地址分配
-将input file的各个段都连在一起,并且为符号分配虚拟地址
+ArrayDeque
++Resizable-array implementation of the Deque interface.
+不允许空
+Array deques have no capacity restrictions; they grow as necessary to support usage.
+not thread-safe【相比于由vector实现的线程安全的Stack】
+This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue.【6】
+fail-fast
+代码:
+ +
public class ArrayDeque<E> extends AbstractCollection<E>
implements Deque<E>, Cloneable, Serializable
{
//非private以让内部类能够访问到
/*
The capacity of the deque is the length of this array,
which is always a power of two. capacity只能是2的幂次
不能满【原理应该跟循环队列差不多,是怕头尾混淆】
但允许短暂的满之后马上扩容
所有不包含元素的数组单元为空
*/
transient Object[] elements;
//The index of the element at the head of the deque
//(which is the element that would be removed by remove() or pop());
//or an arbitrary number equal to tail if the deque is empty.
//head在下标大的地方
transient int head;
//The index at which the 【next】 element would be added to the tail of the deque
//(via addLast(E), add(E), or push(E)).
//tail在下标小的地方
transient int tail;
private static final int MIN_INITIAL_CAPACITY = 8;
// ****** Array allocation and resizing utilities ******
private static int calculateSize(int numElements) {
int initialCapacity = MIN_INITIAL_CAPACITY;
// Find the best power of two to hold elements.
// Tests "<=" because arrays aren't kept full.
if (numElements >= initialCapacity) {
//原理类似HashMap.tableSizeFor
//这一通操作可以得到比cap大的,且离cap最近的2的幂次方数
initialCapacity = numElements;
initialCapacity |= (initialCapacity >>> 1);
initialCapacity |= (initialCapacity >>> 2);
initialCapacity |= (initialCapacity >>> 4);
initialCapacity |= (initialCapacity >>> 8);
initialCapacity |= (initialCapacity >>> 16);
initialCapacity++;
if (initialCapacity < 0) // Too many elements, must back off
initialCapacity >>>= 1;// Good luck allocating 2 ^ 30 elements
}
return initialCapacity;
}
private void allocateElements(int numElements) {
elements = new Object[calculateSize(numElements)];
}
private void doubleCapacity() {
assert head == tail;
int p = head;
int n = elements.length;
int r = n - p; // number of elements to the right of p
int newCapacity = n << 1;
if (newCapacity < 0)
throw new IllegalStateException("Sorry, deque too big");
Object[] a = new Object[newCapacity];
//以head==tail为分界线,右边那段移到开头,左边那段移到后面
System.arraycopy(elements, p, a, 0, r);
System.arraycopy(elements, 0, a, r, p);
elements = a;
head = 0;
tail = n;
}
private <T> T[] copyElements(T[] a) {
if (head < tail) {
System.arraycopy(elements, head, a, 0, size());
} else if (head > tail) {
int headPortionLen = elements.length - head;
System.arraycopy(elements, head, a, 0, headPortionLen);
System.arraycopy(elements, 0, a, headPortionLen, tail);
}
return a;
}
//1
public ArrayDeque() {
elements = new Object[16];
}
public ArrayDeque(int numElements) {
allocateElements(numElements);
}
public ArrayDeque(Collection<? extends E> c) {
allocateElements(c.size());
addAll(c);
}
// The main insertion and extraction methods are addFirst,
// addLast, pollFirst, pollLast. The other methods are defined in
// terms of these.就是说这几个最重要,别的方法都是这四个的附庸
public void addFirst(E e) {
if (e == null)
throw new NullPointerException();
//2
elements[head = (head - 1) & (elements.length - 1)] = e;
if (head == tail)
//队列满
doubleCapacity();
}
public void addLast(E e) {
if (e == null)
throw new NullPointerException();
elements[tail] = e;
if ( (tail = (tail + 1) & (elements.length - 1)) == head)
//队列满
doubleCapacity();
}
public boolean offerFirst(E e) {
addFirst(e);
return true;
}
public boolean offerLast(E e) {
addLast(e);
return true;
}
public E removeFirst() {
E x = pollFirst();
if (x == null)
throw new NoSuchElementException();
return x;
}
public E removeLast() {
E x = pollLast();
if (x == null)
throw new NoSuchElementException();
return x;
}
public E pollFirst() {
int h = head;
E result = (E) elements[h];
// Element is null if deque empty
if (result == null)
return null;
elements[h] = null; // Must null out slot
head = (h + 1) & (elements.length - 1);
return result;
}
public E pollLast() {
int t = (tail - 1) & (elements.length - 1);
E result = (E) elements[t];
if (result == null)
return null;
elements[t] = null;
tail = t;
return result;
}
public E getFirst() {
E result = (E) elements[head];
if (result == null)
throw new NoSuchElementException();
return result;
}
public E getLast() {
E result = (E) elements[(tail - 1) & (elements.length - 1)];
if (result == null)
throw new NoSuchElementException();
return result;
}
public E peekFirst() {
// elements[head] is null if deque empty
return (E) elements[head];
}
public E peekLast() {
return (E) elements[(tail - 1) & (elements.length - 1)];
}
public boolean removeFirstOccurrence(Object o) {
if (o == null)
return false;
//掩码
int mask = elements.length - 1;
int i = head;
Object x;
while ( (x = elements[i]) != null) {
if (o.equals(x)) {
delete(i);
return true;
}
//头->尾
i = (i + 1) & mask;
}
return false;
}
public boolean removeLastOccurrence(Object o) {
if (o == null)
return false;
int mask = elements.length - 1;
int i = (tail - 1) & mask;
Object x;
while ( (x = elements[i]) != null) {
if (o.equals(x)) {
delete(i);
return true;
}
//尾->头
i = (i - 1) & mask;
}
return false;
}
// *** Queue methods ***
public boolean add(E e) {
addLast(e);
return true;
}
public boolean offer(E e) {
return offerLast(e);
}
public E remove() {
return removeFirst();
}
public E poll() {
return pollFirst();
}
public E element() {
return getFirst();
}
public E peek() {
return peekFirst();
}
// *** Stack methods ***
public void push(E e) {
addFirst(e);
}
public E pop() {
return removeFirst();
}
//检查队列情况正常
private void checkInvariants() {
assert elements[tail] == null;
//如果成立,只能是队列空;不成立的话,不能有空元素
assert head == tail ? elements[head] == null :
(elements[head] != null &&
elements[(tail - 1) & (elements.length - 1)] != null);
assert elements[(head - 1) & (elements.length - 1)] == null;
}
//Returns: true if elements moved backwards而不是操作是否成功
private boolean delete(int i) {
checkInvariants();
final Object[] elements = this.elements;
final int mask = elements.length - 1;
final int h = head;
final int t = tail;
final int front = (i - h) & mask;
final int back = (t - i) & mask;
// Invariant: head <= i < tail mod circularity
if (front >= ((t - h) & mask))
throw new ConcurrentModificationException();
// Optimize for least element motion
if (front < back) {
if (h <= i) {
//把i覆盖掉了
System.arraycopy(elements, h, elements, h + 1, front);
} else { // Wrap around
System.arraycopy(elements, 0, elements, 1, i);
elements[0] = elements[mask];
System.arraycopy(elements, h, elements, h + 1, mask - h);
}
elements[h] = null;
head = (h + 1) & mask;
return false;
} else {
if (i < t) { // Copy the null tail as well
System.arraycopy(elements, i + 1, elements, i, back);
//注意没设置空,因为确实不用
tail = t - 1;
} else { // Wrap around
System.arraycopy(elements, i + 1, elements, i, mask - i);
elements[mask] = elements[0];
System.arraycopy(elements, 1, elements, 0, t);
tail = (t - 1) & mask;
}
return true;
}
}
// *** Collection Methods ***
public int size() {
return (tail - head) & (elements.length - 1);
}
public boolean isEmpty() {
return head == tail;
}
//The elements will be ordered from first (head) to last (tail).
//This is the same order that elements would be dequeued
//(via successive calls to remove or popped (via successive calls to pop).
public Iterator<E> iterator() {
return new DeqIterator();
}
public Iterator<E> descendingIterator() {
return new DescendingIterator();
}
private class DeqIterator implements Iterator<E> {
private int cursor = head;
//Tail recorded at construction (also in remove), to stop iterator[怪不得叫fence]
//and also to check for comodification[检查并发].
private int fence = tail;
private int lastRet = -1;
public boolean hasNext() {
return cursor != fence;
}
public E next() {
if (cursor == fence)
throw new NoSuchElementException();
E result = (E) elements[cursor];
// This check doesn't catch all possible comodifications,
// but does catch the ones that corrupt traversal【破坏遍历的】
//tail!=fence说明迭代时修改。
if (tail != fence || result == null)
throw new ConcurrentModificationException();
lastRet = cursor;
cursor = (cursor + 1) & (elements.length - 1);
return result;
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
//3
if (delete(lastRet)) { // if left-shifted, undo increment in next()
cursor = (cursor - 1) & (elements.length - 1);
//update
fence = tail;
}
lastRet = -1;
}
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
Object[] a = elements;
int m = a.length - 1, f = fence, i = cursor;
//4执行完后直接迭代结束
cursor = f;
while (i != f) {
E e = (E)a[i];
i = (i + 1) & m;
if (e == null)
throw new ConcurrentModificationException();
action.accept(e);
}
}
}
private class DescendingIterator implements Iterator<E> {
private int cursor = tail;
//终点为fence,此时终点为head
private int fence = head;
private int lastRet = -1;
public boolean hasNext() {
return cursor != fence;
}
public E next() {
if (cursor == fence)
throw new NoSuchElementException();
cursor = (cursor - 1) & (elements.length - 1);
E result = (E) elements[cursor];
if (head != fence || result == null)
throw new ConcurrentModificationException();
lastRet = cursor;
return result;
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
if (!delete(lastRet)) {
cursor = (cursor + 1) & (elements.length - 1);
fence = head;
}
lastRet = -1;
}
//不支持for-each了吧233
}
public boolean contains(Object o) {
if (o == null)
return false;
int mask = elements.length - 1;
int i = head;
Object x;
while ( (x = elements[i]) != null) {
if (o.equals(x))
return true;
i = (i + 1) & mask;
}
return false;
}
public boolean remove(Object o) {
return removeFirstOccurrence(o);
}
public void clear() {
int h = head;
int t = tail;
if (h != t) { // clear all cells
head = tail = 0;
int i = h;
int mask = elements.length - 1;
do {
elements[i] = null;
i = (i + 1) & mask;
} while (i != t);
}
}
public Object[] toArray() {
return copyElements(new Object[size()]);
}
public <T> T[] toArray(T[] a) {
int size = size();
if (a.length < size)
a = (T[])java.lang.reflect.Array.newInstance(
a.getClass().getComponentType(), size);
copyElements(a);
if (a.length > size)
a[size] = null;
return a;
}
// *** Object methods ***
public ArrayDeque<E> clone() {
try {
ArrayDeque<E> result = (ArrayDeque<E>) super.clone();
result.elements = Arrays.copyOf(elements, elements.length);
return result;
} catch (CloneNotSupportedException e) {
throw new AssertionError();
}
}
private static final long serialVersionUID = 2340985798034038923L;
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {...}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {...}
public Spliterator<E> spliterator() {
return new DeqSpliterator<E>(this, -1, -1);
}
static final class DeqSpliterator<E> implements Spliterator<E> {...}
}其中:
+
- -
默认容量
空构造器的默认容量为16
符号解析与重定位
--
+扫描所有输入文件的符号表形成全局符号表;
+- +
(head - 1) & (elements.length - 1)
是一个便捷的截位取余操作,这跟hashmap一个原理,详见hashmap第二点。
+- -
if (delete(lastRet))
delete方法返回true,说明右移数组,此时next指针需要++
+delete方法返回false,说明左移数组,此时next指针不变
重定向
-可执行文件基本可以确定自己在进程虚拟空间中的起始位置,因为可执行文件往往是第一个被加载的文件,它可以选择一个固定空闲的地址。因而,在link中,可执行文件的地址都以确定,就可以开始进行重定向。
-通过重定向表对所有UNDEF的符号进行地址修正,包括相对地址修正和绝对地址修正。
+forEachRemaining
跟差不多所有的迭代器实现一样,此方法执行完毕之后,cursor直接跳到数组最末,相当于迭代结束
List
List(I)
有序、支持随机访问
+代码
+ +
/*
List 接口提供了一个特殊的迭代器,称为 ListIterator,
除了 Iterator 接口提供的正常操作之外,它还允许元素插入和替换以及双向访问。
List 接口提供了两种方法来搜索指定的对象。 从性能的角度来看,应谨慎使用这些方法。
在许多实现中,它们将执行代价高昂的线性搜索。
Note: While it is permissible for lists to contain themselves as elements, extreme caution is advised: the equals and hashCode methods are no longer well defined on such a list.【这跟上面的引用点4是一样的。】
*/
public interface List<E> extends Collection<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
//Returns true if and only if the specified object is also a list,
//both lists have the same size, and all corresponding pairs of elements in the two lists are equal.
//In other words, two lists are defined to be equal if they contain the same elements in the same order.
//注意,对于未重载equal方法的类,引用对象的相等指的是地址相等也就是说必须是一模一样的对象,两个不同对象但值相同,这种情况是不算equal的。
boolean equals(Object o);
int hashCode();
//newly add or change below
//将此列表的每个元素替换为将运算符应用于该元素的结果。
default void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final ListIterator<E> li = this.listIterator();
while (li.hasNext()) {
li.set(operator.apply(li.next()));
}
}
//@SuppressWarings注解 作用:用于抑制编译器产生警告信息。
default void sort(Comparator<? super E> c) {
Object[] a = this.toArray();
//借助Arrays的sort方法
Arrays.sort(a, (Comparator) c);
ListIterator<E> i = this.listIterator();
//再线性逐一替换
for (Object e : a) {
i.next();
// set:
// Replaces the last element returned by next or previous
// with the specified element
i.set((E) e);
}
}
E get(int index);
E set(int index, E element);
//插入元素,把当前位及其以后的元素都往后挪一位
void add(int index, E element);
//移除元素,把当前位及其以后的元素都往前挪一位
E remove(int index);
//-1 if this list does not contain the element
//ClassCastException:if the type of the specified element
//is incompatible with this list
int indexOf(Object o);
ListIterator<E> listIterator();
//指定的索引指示初始调用next将返回的第一个元素.对 previous 的初始调用将返回具有指定索引减一的元素。
ListIterator<E> listIterator(int index);
//[fromIndex,toIndex).If fromIndex==toIndex then return==null.
List<E> subList(int fromIndex, int toIndex);
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.ORDERED);
}
}其中:
+
-- +
hashcode不允许自环
Note: While it is permissible for lists to contain themselves as elements, extreme caution is advised: the equals and hashCode methods are no longer well defined on such a list.【这跟上面的引用点4是差不多的。】
+
ArrayList l1 = new ArrayList();
l1.add(l1);
System.out.println(l1.hashCode());
//输出:Stack Overflow- -
structural and non-structural change in list
在link中,会读取
-test.o
以及lib.a
中的符号表,完成重定向(绝对地址和相对地址)以及节的重排组织,最终组合形成以段为单位的可执行文件test
。可执行文件
-test
会通过系统调用exevec
被装载进物理内存(lazy allocation),分段映射到进程的虚拟地址空间。静态链接的缺陷是,由于重定向在link过程完成,故而同一份共享库在物理内存中会有多份copy,极大占用物理内存和磁盘空间。优点是速度快。
-动态链接
(下文注意区分两个概念:可执行文件和动态链接库)
-动态链接库(.so)不同于静态链接库。
-+
test.c ——(compile)——>test.o——(link)——>test(ELF exe)
↑
lib.so关于sublist
① view
sublist的document有一个很有趣的点,就是把sublist称为原list的视图view,这不禁让人想起了数据库里的表和表的视图。
+但是还是有差别的。
+在数据库中,视图仅仅是表或表的一部分的快照,修改视图对原表没有影响。但此处,对sublist的结构性修改和非结构性修改都会使原list的对应元素发生改变。
+但有一点是相同的。如果对原表/原list修改,那么视图就会没用/会寄掉。
+可以像这样来对主list指定范围内的元素进行操作,免去复杂的下标。
+-
//For example, the following idiom removes a range of elements from a list:
list.subList(from, to).clear();在link中,仅会读入动态链接库的符号表,对于动态链接库的符号仅会将其标记为动态符号,而不会对其进行重定向。
-可执行文件
-test
会通过系统调用exevec
被装载进物理内存(lazy allocation),分段映射到进程的虚拟地址空间。静态链接是per-process一份库,内存中有多份库;动态链接是per-process一份库,内存也只有一份库。并且虚拟地址动态分配,也即库映射到进程地址空间的哪块VMA是不确定的。
-由于动态链接库被装载时的虚拟地址不确定,所以对于动态链接库和可执行文件代码中与动态链接相关的绝对地址,不能简单采用装载时重定向的方法来对其重定向,否则会破坏其共享性和不变性。
---试想一下,每个进程加载的动态链接库的地址都不同,那岂不是每个进程的动态链接库的重定向结果都不一样,指令都不一样,不就寄了。
-所以我们此时进行了一个牛逼到家、惊天动地、无人能比的操作。
-我们可以分离.text和.data,前者作为“共享”语义保持不变性,后者则在每个进程地址空间中都留存一个copy。然后,我们将所有立即数绝对寻址的地方,换为间接寻址!也即,把那个立即数绝对地址改成一个变量,变量值在.data段中存储。这样一来,就成功把绝对寻址替换成了相对寻址。加载的时候也只需将虚拟地址填进可变的.data就行。不得不说真是十分地巧妙。
-这个从相对地址——绝对地址的转换过程,由ELF中的一个新段GOT表(.got)来实现。在link时加入了动态链接库符号表的可执行文件,以及动态链接库本身,都使用了.got段,以PIC形式出现。
-这个操作就是所谓的“地址无关代码”,通过
--fPIC
选项,就可以将代码编译为一个地址无关的程序。使用PIC模式编译的共享对象,对于模块内部的函数调用也是采用跟模块外部函数调用一样的方式,即使用GOT/PLT的方式。--小trick:如何区分一个DSO是否为PIC
-+
readelf -d foo.so | grep TEXTREL② 留下一个问题
-
public class Main {
public static void main(String[] args) {
ArrayList<Student> a=new ArrayList<>();
a.add(new Student("Lily",16));
a.add(new Student("Sam",16));
a.add(new Student("Tom",16));
a.add(new Student("Mary",16));
a.add(new Student("Mark",16));
a.add(new Student("John",16));
List<Student> suba=a.subList(2,5);
System.out.println("Firstly,suba:");
printall(suba);
System.out.println("Change the value of age:");
suba.get(1).age=300;
System.out.println("suba:");
printall(suba);
System.out.println("a:");
printall(a);
System.out.println("Change the address:");
suba.set(1,new Student("haha",200));
System.out.println("suba:");
printall(suba);
System.out.println("a:");
printall(a);
System.out.println("Change the structure of sublist:");
suba.remove(0);
System.out.println("suba:");
printall(suba);
System.out.println("a:");
printall(a);
}
public static void printall(List<Student> list){
for (Student a : list){
System.out.print(a+"\t");
}
System.out.println();
}
}
/*运行结果
Firstly,suba:
name :Tomage :16 name :Maryage :16 name :Markage :16
Change the value of age:
suba:
name :Tomage :16 name :Maryage :300 name :Markage :16
a:
name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Maryage :300 name :Markage :16 name :Johnage :16
Change the address:
suba:
name :Tomage :16 name :hahaage :200 name :Markage :16
a:
name :Lilyage :16 name :Samage :16 name :Tomage :16 name :hahaage :200 name :Markage :16 name :Johnage :16
Change the structure of sublist:
suba:
name :hahaage :200 name :Markage :16
a:
name :Lilyage :16 name :Samage :16 name :hahaage :200 name :Markage :16 name :Johnage :16
*/如果上面的命令有任何输出,那么foo.so就不是PIC的,否则就是PIC的。PIC的DSO是不会包含任何代码段重定位表的,TEXTREL表⽰代码段重定位表地址。
-这也很好理解,因为PIC本质上就是把代码段重定位转化为了数据段重定位。
-除了动态链接库中的寻址(对变量和函数)需要使用PIC之外,对可执行文件的全局变量也需要使用特殊的机制。ELF共享库中的全局变量都类似以弱引用形式存在。当全局变量在主程序extern时,若该变量在共享库中初始化了,那么加载之后要把共享库的数据copy进主程序;否则,该变量值都以主模块为准。
---这段原因解释看书真没懂,详情340页开始。
-不过感觉它可能说的有点问题,我个人认为全局变量需要使用这种以方式存在,是为了保证进程资源独立。如果变量都以共享库中的数据值为准,那各个进程共享共享库不就乱了。。。你改一下我改一下
-因而,总的装载流程是:
-未优化情况下,在可执行文件被装载之前,先将其依赖的所有动态链接库加载进内存。若其所需的动态链接库已经被映射到物理内存,则将其装载到进程虚拟地址空间;否则,则映射到物理内存,并且装载到进程虚拟地址空间。然后,在装载动态链接库后,扫描可执行文件.got段符号进行装载时重定向(依据已经装载了的动态链接库虚拟地址来计算符号地址)即可。
-但可以注意到这一步还是有优化空间。所以我们采取延迟绑定(PLT)的方法,第一次访问到动态链接库符号时,才对其进行重定向并填入.got中。
-动态链接的缺点就是太慢了,一是因为PIC导致模块内部函数和全局变量也需要以.got形式访问,加了层寻址;二是运行时重定向开销巨大。对于前者,模块内部函数可以使用static关键字修饰;对于后者,采用PLT。
--
显式运行时链接
支持动态链接的系统往往都支持一种更加灵活的模块加载方式,叫做显式运行时链接(Explicit Runtime Linking),有时候也叫做运行时加载。也就是让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。
+为什么之前的toArray得到的array换了个对象,原list不会变【详见Collection说明第五点】;而这里的sublist得到的子list换了个对象,原list也会变呢?
+也许这跟间接寻址的级数有关?之后看过ArrayList的具体实现再来解答。
--也就是说,前面介绍的动态链接库是由动态链接器自动完成的,程序啥也不知道;这里的动态装载库是程序自己控制的,所以会提供给程序各种API。
+穿越回来:
+ toArray得到array,是新开辟了存储空间,里面存放了原list对象的地址。因而,修改新存储空间的地址内容,原list是不变的。
+ 但sublist连新开辟存储空间也没有,其差不多所有操作都是从list进行的。因而它换了个对象,具体实现就是让原list也换了个对象。
+ 所以说其实前者更像是数据库里面的“视图”概念。
而动态库的装载则是通过一系列由动态链接器提供的API,具体地讲共有4个函数:打开动态库(
+dlopen
)、查找符号(dlsym
)、错误处理(dlerror
)以及关闭动态库(dlclose
),程序可以通过这几个API对动态库进行操作。这几个API的实现是在/lib/libdl.so.2
里面,它们的声明和相关常量被定义在系统标准头文件<dlfcn.h>
。③ 关于sublis和原list、non-structral和structural的区别:
Can structural changes in sublist reflected in the original list, in JAVA?
--很有意思的是,如果我们将filename这个参数设置为0,那么dlopen返回的将是全局符号表的句柄,也就是说我们可以在运行时找到全局符号表里面的任何一个符号,并且可以执行它们,这有些类似高级语言反射(Reflection)的特性。全局符号表包括了程序的可执行文件本身、被动态链接器加载到进程中的所有共享模块以及在运行时通过dlopen打开并且使用了RTLD_GLOBAL方式的模块中的符号。
-它接下来举的例子很有意思,可惜不知道为啥在我这一直segment fault。。。好像是它用的内联汇编是32位什么的,我折腾了半天还是没办法,算了
-+
/*
我们这个例子中将实现一个更为灵活的叫做runso的程序,这个程序可以通过命令行来执行共享对象里面的任意一个函数。
它在理论上很简单,基本的步骤就是:由命令行给出共享对象路径、函数名和相关参数,然后程序通过运行时加载将该模块加载
到进程中,查找相应的函数,并且执行它,然后将执行结果打印出来。
为了表示参数和返回值类型,我们假设字母d表示double、i表示int、s表示char*、v表示void
比如说,如果要调用/lib/libfoo.so里面一个void bar(char* str, int i)的函数,可以使用如下命令行:
$./RunSo /lib/libfoo.so bar sHello i10
*/
// 大概就是根据参数类型把参数压入栈
// 大概就是相当于pop,给esp加上我们之前申请的栈空间esp
int main(int argc, char* argv[])
{
void* handle;
char* error;
int i;
int esp = 0;
void* func;
handle = dlopen(argv[1], RTLD_NOW);
if(handle == 0) {
printf("Can't find library: %s\n", argv[1]);
return -1;
}
func = dlsym(handle, argv[2]);
if( (error = dlerror()) != NULL ) {
printf("Find symbol %s error: %s\n", argv[2], error);
goto exit_runso;
}
// 根据返回值不同构造函数指针
switch(argv[argc-1][0]){
case 'i':
{
int (*func_int)() = func;
SETUP_STACK;
int ret = func_int();
RESTORE_STACK;
printf("ret = %d\n", ret );
break;
}
case 'd':
{
double (*func_double)() = func;
SETUP_STACK;
double ret = func_double();
RESTORE_STACK;
printf("ret = %f\n", ret );
break;
}
case 's':
{
char* (*func_str)() = func;
SETUP_STACK;
char* ret = func_str();
RESTORE_STACK;
printf("ret = %s\n", ret );
break;
}
case 'v':
{
void (*func_void)() = func;
SETUP_STACK;
func_void();
RESTORE_STACK;
printf("ret = void");
break;
}
} // end of switch
exit_runso:
dlclose(handle);
}In the document of List class:
++
/*
The returned list is backed by this list, so non-structural changes in the returned list are reflected in this list, and vice-versa.
*/
List<E> subList(int fromIndex, int toIndex);Can structural changes in sublist reflected in the original list in JAVA?
+答案是会改变的。实例可看问题中代码,或者point3的代码。
+但是既然都会变的话,为什么文档里面要特意强调“non-structural”呢?这是因为,对sublist进行结构性改变会让原list也正常地一起变,但是对原list进行结构性改变却会让sublist寄掉:
+文档下面接着写道:
++
The semantics of the list returned by this method become undefined if the backing list (i.e., this list) is structurally modified in any way other than via the returned list. (Structural modifications are those that change the size of this list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.)
/*
sublist会寄,如果其原list被结构性改变了,且是以除了通过sublist结构性改变的方式外的其他方式改变的。(结构性改变是指那些会改变list的长度,或者会使迭代失败的那些改变)
*/通过代码验证可得,确实寄了
+-
public static void main(String[] args) {
ArrayList<Student> a=new ArrayList<>();
a.add(new Student("Lily",16));
a.add(new Student("Sam",16));
a.add(new Student("Tom",16));
a.add(new Student("Mary",16));
a.add(new Student("Mark",16));
a.add(new Student("John",16));
List<Student> suba=a.subList(2,5);
System.out.println("Firstly,suba:");
printall(suba);
printall(a);
System.out.println("Change the origin structure:");
a.remove(3);
System.out.println("a:");
printall(a);
System.out.println("suba:");
printall(suba);
}
/*运行结果
Firstly,suba:
name :Tomage :16 name :Maryage :16 name :Markage :16
name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Maryage :16 name :Markage :16 name :Johnage :16
Change the origin structure:
a:
name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Markage :16 name :Johnage :16
suba:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1241)
*/运行库
初始化
操作系统装载程序之后,首先运行的代码并不是main的第一行,而是某些别的代码,这些代码负责准备好main函数执行所需要的环境,并且负责调用main函数,这时候你才可以在main函数里放心大胆地写各种代码:申请内存、使用系统调用、触发异常、访问I/O。在main返回之后,它会记录main函数的返回值,调用atexit注册的函数,然后结束进程。
-运行这些代码的函数称为入口函数或入口点(Entry Point),视平台的不同而有不同的名字。程序的入口点实际上是一个程序的初始化和结束部分,它往往是运行库的一部分。
-一个典型的程序运行步骤大致如下:
+通过以上可以感觉,估计sublist和原list的元素是共享存储空间的,只不过可能对象里有相关维护的变量。
+Any method accessing the list through the sub list effectively does index + offset.
故而要是list变了,sublist的相关维护变量不变,就会依然傻傻地进行offset+index操作,这样就会寄。此猜想有待验证23333+- -穿越回来:
-
-- -
操作系统在创建进程后,把控制权交到了程序的入口,这个入口往往是运行库中的某个入口函数。
-- -
入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造,等等。
-- -
入口函数在完成初始化之后,调用main函数,正式开始执行程序主体部分。
-- +
main函数执行完毕以后,返回到入口函数,入口函数进行清理⼯作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程。
-- 确实是共享存储空间的,不如说sublist就直接引用了原list的变量,所有操作实质上都是在原list上进行。
+- sublist傻傻地进行offset+index操作,这样体现在原list上,可能导致下标越界或者结果并非我们想要的。
Linux中的C语言运行库就是glibc。
-运行库
运行时库(Runtime Library)为入口函数及其所依赖的函数所构成的函数、各种标准库函数的实现的集合。可以通过
-sudo apt-get install glibc-source
安装glibc的源代码。一个C语言运行库大致包含了如下功能:
--
-- -
启动与退出:包括入口函数及入口函数所依赖的其他函数等。
-- -
标准函数:由C语言标准规定的C语言标准库所拥有的函数实现。
-- -
I/O:I/O功能的封装和实现,参见上一节中I/O初始化部分。
-应该指的是比如说提供File*指针、IO stream之类的高级功能封装。
-- -
堆:堆的封装和实现,参见上一节中堆初始化部分。
--这点让我耳目一新!因为我以前一直以为堆栈都是操作系统实现的,现在想来才发现确实,操作系统只负责通过sbrk系统调用给内存,具体的堆分配算法由glibc的malloc实现。
- -
语言实现:语言中一些特殊功能的实现。
-调试:实现调试功能的代码。
-库函数介绍
它这里主要讲了两个比较特殊的库,还挺有意思的:变长参数(stdarg.h)和非局部跳转(setjmp.h)。
--
变长参数
-讲这玩意其实用作是printf的实现。看下下面这两个代码相信你就能明白printf的基本原理了:
-+
// Code 1
int sum(unsigned num, ...)
{
int* p = &num + 1;
int ret = 0;
while (num--)
ret += *p++;
return ret;
}
call:
int n = sum(3, 16, 38, 53);AbstractList(A)
++提供随机访问list的基本骨架
+To implement an unmodifiable list, the programmer needs only to extend this class and provide implementations for the get(int) and size() methods.
+To implement a modifiable list, the programmer must additionally override the set(int, Object) method
+If the list is variable-size,the programmer must additionally override the add(int, E) and remove(int) methods.
+Unlike the other abstract collection implementations, the programmer does not have to provide an iterator implementation; the iterator and list iterator are implemented by this class, on top of the “random access“ methods: get(int), set(int, E), add(int, E) and remove(int).这里的迭代器反而是通过类里面的方法来实现的
+代码
-
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
protected AbstractList() {
}
//重写add方法
public boolean add(E e) {
//此为下面的public void add(int index, E element);
add(size(), e);
return true;
}
//交给具体实现
abstract public E get(int index);
//假设不能修改
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
// Search Operations
public int indexOf(Object o) {
ListIterator<E> it = listIterator();
if (o==null) {
while (it.hasNext())
if (it.next()==null)
return it.previousIndex();
} else {
while (it.hasNext())
if (o.equals(it.next()))
return it.previousIndex();
}
return -1;
}
public int lastIndexOf(Object o) {
//从后往前遍历
ListIterator<E> it = listIterator(size());
if (o==null) {
while (it.hasPrevious())
if (it.previous()==null)
return it.nextIndex();
} else {
while (it.hasPrevious())
if (o.equals(it.previous()))
return it.nextIndex();
}
return -1;
}
// Bulk Operations
public void clear() {
removeRange(0, size());
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
boolean modified = false;
for (E e : c) {
add(index++, e);
modified = true;
}
return modified;
}
// Iterators
public Iterator<E> iterator() {
return new Itr();
}
public ListIterator<E> listIterator() {
return listIterator(0);
}
public ListIterator<E> listIterator(final int index) {
rangeCheckForAdd(index);
return new ListItr(index);
}
//内部类诶 1
private class Itr implements Iterator<E> {
//Index of element to be returned by subsequent call to next.
int cursor = 0;
//Index of element returned by most recent call to next or previous. Reset to -1 if this element is deleted by a call to remove.
int lastRet = -1;
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size();
}
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0;
}
public E previous() {
checkForComodification();
try {
int i = cursor - 1;
E previous = get(i);
//lastRet=i;cursor=i;
lastRet = cursor = i;
return previous;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.set(lastRet, e);
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
AbstractList.this.add(i, e);
lastRet = -1;
cursor = i + 1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
//迭代器定义结束
public List<E> subList(int fromIndex, int toIndex) {
//Sublist和RandomAccessSubList在后面都作为内部类定义
//这俩的分支主要是能否支持高性能随机访问,而这点在Java是依靠是否实现RandomAccess接口
//来体现的,要实现接口必须得是一个类。因此,为了分歧,这里不得不创建两个类来表示两种情况。
//这两个类的方法应该是大致相同的。
//2
return (this instanceof RandomAccess ?
new RandomAccessSubList<>(this, fromIndex, toIndex) :
new SubList<>(this, fromIndex, toIndex));
}
// Comparison and hashing
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof List))
return false;
ListIterator<E> e1 = listIterator();
ListIterator<?> e2 = ((List<?>) o).listIterator();
while (e1.hasNext() && e2.hasNext()) {
E o1 = e1.next();
Object o2 = e2.next();
//如果这东西没有重载equals方法,那此处就是单纯object对象是否相同了
if (!(o1==null ? o2==null : o1.equals(o2)))
return false;
}
//为啥size不一样不在一开始就比呢?那样不是更省花销吗
return !(e1.hasNext() || e2.hasNext());
}
public int hashCode() {
int hashCode = 1;
for (E e : this)
hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
return hashCode;
}
protected void removeRange(int fromIndex, int toIndex) {
ListIterator<E> it = listIterator(fromIndex);
for (int i=0, n=toIndex-fromIndex; i<n; i++) {
it.next();
it.remove();
}
}
//The number of times this list has been structurally modified.
//3
protected transient int modCount = 0;
private void rangeCheckForAdd(int index) {
if (index < 0 || index > size())
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size();
}
}
class SubList<E> extends AbstractList<E> {
//这大概是对父list的引用
private final AbstractList<E> l;
//这大概是在父list的起始偏移量
private final int offset;
//sublist的长度
private int size;
SubList(AbstractList<E> list, int fromIndex, int toIndex) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > list.size())
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
l = list;
offset = fromIndex;
size = toIndex - fromIndex;
this.modCount = l.modCount;
}
public E set(int index, E element) {
rangeCheck(index);
checkForComodification();
//改变sublist也会改变父list,是否成功取决于对父list的改变是否成功
return l.set(index+offset, element);
}
public E get(int index) {
rangeCheck(index);
checkForComodification();
//直接取父类值
return l.get(index+offset);
}
public int size() {
checkForComodification();
return size;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
checkForComodification();
l.add(index+offset, element);
this.modCount = l.modCount;
size++;
}
public E remove(int index) {
rangeCheck(index);
checkForComodification();
E result = l.remove(index+offset);
this.modCount = l.modCount;
size--;
return result;
}
protected void removeRange(int fromIndex, int toIndex) {
checkForComodification();
l.removeRange(fromIndex+offset, toIndex+offset);
this.modCount = l.modCount;
size -= (toIndex-fromIndex);
}
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
int cSize = c.size();
if (cSize==0)
return false;
checkForComodification();
l.addAll(offset+index, c);
this.modCount = l.modCount;
size += cSize;
return true;
}
public Iterator<E> iterator() {
return listIterator();
}
public ListIterator<E> listIterator(final int index) {
checkForComodification();
rangeCheckForAdd(index);
//改变迭代器实现
return new ListIterator<E>() {
//子类迭代器=父类迭代器+offset
private final ListIterator<E> i = l.listIterator(index+offset);
public boolean hasNext() {
return nextIndex() < size;
}
public E next() {
if (hasNext())
return i.next();
else
throw new NoSuchElementException();
}
public boolean hasPrevious() {
return previousIndex() >= 0;
}
public E previous() {
if (hasPrevious())
return i.previous();
else
throw new NoSuchElementException();
}
public int nextIndex() {
return i.nextIndex() - offset;
}
public int previousIndex() {
return i.previousIndex() - offset;
}
public void remove() {
i.remove();
SubList.this.modCount = l.modCount;
size--;
}
public void set(E e) {
i.set(e);
}
public void add(E e) {
i.add(e);
SubList.this.modCount = l.modCount;
size++;
}
};
}
//层层套娃啊
public List<E> subList(int fromIndex, int toIndex) {
return new SubList<>(this, fromIndex, toIndex);
}
private void rangeCheck(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private void rangeCheckForAdd(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
private void checkForComodification() {
if (this.modCount != l.modCount)
throw new ConcurrentModificationException();
}
}
//确实实现上没什么区别,主要是多了个RandomAccess的约定。
class RandomAccessSubList<E> extends SubList<E> implements RandomAccess {
RandomAccessSubList(AbstractList<E> list, int fromIndex, int toIndex) {
super(list, fromIndex, toIndex);
}
public List<E> subList(int fromIndex, int toIndex) {
return new RandomAccessSubList<>(this, fromIndex, toIndex);
}
}+
// Code 2
void arg_match(const char* fmt, ...) {
va_list ap; // 本质char * / void *
va_start(ap, fmt); // 之后ap就会指向fmt后的第一个可变参数
int idx = 0;
for (int i = 0; i < strlen(fmt); i ++) {
if (fmt[i] != '%') continue;
idx ++;
switch (fmt[i + 1]) {
case 'd':
int argv_i = va_arg(ap, int);
printf("第%d个参数为:%d\n", idx, argv_i);
break;
case 's':
char* argv_s = va_arg(ap, char*);
printf("第%d个参数为:%s\n", idx, argv_s);
break;
default:
printf("unknown.\n");
break;
}
}
}
call:
arg_match("%d %d %s\n", 1, 2, "333");其中:
+
-- +
内部类ListItr和Itr的实现
Itr的实现需要用到AbstratcList类的get和set方法,而显然不同Collection的get和set不一样。为了避免混淆,Itr就只能作为私有类。为了避免胡乱引用,Itr就可以直接作为内部类,共享其外部类的所有资源。
+ListItr作为内部私有类很容易理解,毕竟只有list才需要它。
+- +
RandomAccess
RandomAccess是一个空接口,它应该代表一个约定俗成的规定,即它的implementations的随机访问都是性能较高的。这个空接口思想很常见,源码带给我们的智慧。
+
按经验来说, a List implementation should implement this interface, if this loop:
for (int i=0, n=list.size(); i < n; i++)
list.get(i);
runs faster than this loop:
for (Iterator i=list.iterator(); i.hasNext(); )
i.next();- -
关于modCount字段与fail-fast机制
+modCount字段就是保证一定程度并发安全的变量,fail-fast就是指马上抛出异常。
+-
除此之外,我们也可以实现变长参数宏:
-在GCC编译器下,变长参数宏可以使用“##”宏字符串连接操作实现。
-
非局部跳转
-这位更是重量级
-+
jmp_buf b;
void f()
{
longjmp(b, 1);
}
int main()
{
if (setjmp(b))
printf("World!");
else
{
printf("Hello ");
f();
}
}modCount不能保证绝对的并发安全,因为它只负责防范结构改变,而不负责看某位置的数据更新。
+在现实中要实现对集合的边迭代边修改,下面三种方式都是错的:
+-
事实上的输出是:
-Hello World!
+第一种方法也可以把for里的size换成list.size()
+其中乐观锁与悲观锁可见:乐观锁
-实际上,当setjmp正常返回的时候,会返回0,因此会打印出“Hello”的字样。而longjmp的作用,就是让程序的执行流回到当初setjmp返回的时刻,并且返回由longjmp指定的返回值(longjmp的参数2),也就是1,自然接着会打印出“World!”并退出。换句话说,longjmp可以让程序“时光倒流”回setjmp返回的时刻,并改变其行为,以至于改变了未来。
++
+- 乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。
+++注意“在此期间”的含义是拿到数据到更新数据的这段时间。因为没有加锁,所以别的线程可能会更改。还有一点那就是乐观锁其实是不加锁的来保证某个变量一系列操作原子性的一种方法。
++
- 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中
+synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。glibc
glibc是对标准C运行库的扩展(如增加了pthread),全称GNU C Library,是GNU旗下的C标准库。
-生命周期
于是,我们可以完整串联整个运行程序的生命周期:
-由链接器ld将所有.o文件的
-_init
段和_finit
段(包含glibc对堆空间的初始化和释放、编译器对C++全局对象构造析构的实现以及app自己实现的init和finit函数)分别串在一起,并且链接上glibc库的包含了_start
(会调用_init
)的crt.o文件,最后就形成了包含各种glibc标准库和真·用户代码的可执行文件。可执行文件被装载到进程地址空间后,首先会进行动态链接。然后,从程序入口
-_start
开始进行各种初始化,调用可执行文件的这个_init
段的内容。init完成之后,glibc就调用程序中的入口main。main执行过程中会用到glibc的各种标准库函数。main执行完后就会继续执行_finit
段来结束一切。C++的全局对象构造析构
-
构造(
-_init
)编译器会将每个全局对象的构造函数以如下形式包装:
-+
static void GLOBAL__I_Hw(void)
{
Hw::Hw(); // 构造对象
atexit(__tcf_1); // 一个神秘的函数叫做__tcf_1被注册到了exit
}AbstractList帮我们实现了差不多所有方法,除了Tget(int) 、size() 、set(int, Object) 、add(int, E) 、remove(int) 。因而,接下来的两个实现中,重点关注这些就行。
+ArrayList
代码
++Implements all optional list operations, and permits all elements, including null.
+This class is roughly equivalent to Vector, except that it is unsynchronized.
+size(),isEmpty(),get(),set(),iterator(),listIterator()的时间复杂符是**常量级别(constant time),add()方法的时间复杂度是可变常量级别(amortized constant time),即为O(n)。大致上说,剩余方法的时间复杂度都是线性时间(linear time)。相较于LinkedList的实现,ArrayList的常量因子(constant factor)**较低。
+An application can increase the capacity of an ArrayList instance before adding a large number of elements using the ensureCapacity operation. This may reduce the amount of incremental reallocation.
+本身是线程不安全的,但可以通过封装类来实现线程同步。可以使用Collections.synchronizedList method.
++
List list = Collections.synchronizedList(new ArrayList(...));总体来说实现的很多方法跟想象中差别不大,有几个比较惊艳
+-
//1 cloneable
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//4
private static final long serialVersionUID = 8683452581122892189L;
//可以注意一下,初始=10.
private static final int DEFAULT_CAPACITY = 10;
//Shared empty array instance used for empty instances.
//7
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//5、6
/*
The array buffer into which the elements of the ArrayList are stored.缓冲
ArrayList的容量=此数组的length
Any empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
private int size;
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//in the order they are returned by c's iterator.
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
//size==0
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
//把容量缩小为当前size。An application can use this operation to minimize the storage of an ArrayList instance.
public void trimToSize() {
//涉及List的结构性改变
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
//8
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//如果原size!=0,除非minCapacity=0,否则必须是要扩容一次的【函数要求】,
//因此设置为最小值0,以确保下面的if条件一定为true。
? 0
//如果为默认大小0,此处是必须扩为默认大小的
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
//修改list结构
//若minCapacity<elementData.length,本句modCount++始终执行。
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// overflow-conscious code
//capacity=length of elementData
int oldCapacity = elementData.length;
//每次扩1/2
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//防止溢出
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
//2
public Object clone() {
try {
//from Object.clone():
//the returned object should be obtained by calling super.clone.
ArrayList<?> v = (ArrayList<?>) super.clone();
//浅拷贝,只拷贝地址
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
// Positional Access Operations
E elementData(int index) {
return (E) elementData[index];
}
//这里我挺迷惑的,为什么还要再套一层elementData??
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
public boolean add(E e) {
//既增加了modcount,也保证了capacity够用
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);//src dest 移动数组
elementData[index] = element;
size++;
}
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work,自动清除无引用对象
return oldValue;
}
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
//这不是跟上面一模一样吗,为啥还要再写一遍?
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
public void clear() {
modCount++;
//9 我能不能直接猛一点:elementData=new Object[elementData.length]?
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
// clear to let GC do its work
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
size = newSize;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
//这个complement做得很漂亮,兼顾实际意义又统一了代码
private boolean batchRemove(Collection<?> c, boolean complement) {
//局部变量
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
//原地平移,nice
//如果是removeall,此条件成立说明c不含有该元素,则保留该元素
//如果是retainall,此条件成立说明c含有该元素,则保留该元素
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
//序列化
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
/*from ObjectOutputStream:
Write the non-static and non-transient fields of the current class
to this stream.*/
s.defaultWriteObject();
//为啥要特地强调write size?
s.writeInt(size);
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
//保证一定基础的序列化同步
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
//不大懂为什么这里的值被ignore了?
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
public ListIterator<E> listIterator(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
public ListIterator<E> listIterator() {
return new ListItr(0);
}
public Iterator<E> iterator() {
return new Itr();
}
/**
* AbstractList.Itr的优化版本
*/
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
//内部类访问外部类this的方法
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
//change here
//确实AbstractList的那个remove应该更适用于LinkedList
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
//这里不抛异常吗
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
//好像确实是并发修改了,毕竟上面已经test过i<size<capacity了
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
//10
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
/**
* AbstractList.ListItr的优化版本
*/
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
super();
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0;
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
//11 防止并发修改,比如在这期间进行了trim
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
static void subListRangeCheck(int fromIndex, int toIndex, int size) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > size)
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
}
//依然内部类。不过这里应该用了个作用域的性质。相比于AbstractList定义在List类外部的
//Sublist,应该会更优先使用定义在内部的Sublist
//12 注意,这里的sublist没有直接extends ArrayList
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
//新增成员
private final int parentOffset;
private final int offset;
int size;
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
//这个parentOffset应该是指相较于最近的父亲的偏移量,offset应该就是相较于最底层的父亲的偏移量
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
public E set(int index, E e) {
rangeCheck(index);
checkForComodification();
E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
}
public E get(int index) {
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index);
}
public int size() {
checkForComodification();
return this.size;
}
public void add(int index, E e) {
rangeCheckForAdd(index);
checkForComodification();
//13
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
this.size++;
}
public E remove(int index) {
rangeCheck(index);
checkForComodification();
E result = parent.remove(parentOffset + index);
this.modCount = parent.modCount;
this.size--;
return result;
}
protected void removeRange(int fromIndex, int toIndex) {
checkForComodification();
parent.removeRange(parentOffset + fromIndex,
parentOffset + toIndex);
this.modCount = parent.modCount;
this.size -= toIndex - fromIndex;
}
public boolean addAll(Collection<? extends E> c) {
return addAll(this.size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
int cSize = c.size();
if (cSize==0)
return false;
checkForComodification();
parent.addAll(parentOffset + index, c);
this.modCount = parent.modCount;
this.size += cSize;
return true;
}
public Iterator<E> iterator() {
return listIterator();
}
public ListIterator<E> listIterator(final int index) {
checkForComodification();
rangeCheckForAdd(index);
final int offset = this.offset;
return new ListIterator<E>() {
int cursor = index;
int lastRet = -1;
int expectedModCount = ArrayList.this.modCount;
public boolean hasNext() {
//内部内部类还可以访问外部类和外部外部类
return cursor != SubList.this.size;
}
public E next() {
checkForComodification();
int i = cursor;
if (i >= SubList.this.size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (offset + i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[offset + (lastRet = i)];
}
public boolean hasPrevious() {
return cursor != 0;
}
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (offset + i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[offset + (lastRet = i)];
}
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = SubList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (offset + i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[offset + (i++)]);
}
// update once at end of iteration to reduce heap write traffic
lastRet = cursor = i;
checkForComodification();
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
SubList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = ArrayList.this.modCount;
} catch (IndexOutOfBoundsException ex) {
//确实要是IndexOutOfBounds的话,就说明lastRet改了,说明Concurrent了
throw new ConcurrentModificationException();
}
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.set(offset + lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
SubList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = ArrayList.this.modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (expectedModCount != ArrayList.this.modCount)
throw new ConcurrentModificationException();
}
};
}
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, offset, fromIndex, toIndex);
}
public Spliterator<E> spliterator() {
checkForComodification();
return new ArrayListSpliterator<E>(ArrayList.this, offset,
offset + this.size, this.modCount);
}
}
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
//一修改就会寄
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
public Spliterator<E> spliterator() {
return new ArrayListSpliterator<>(this, 0, -1, 0);
}
/** Index-based split-by-two, lazily initialized Spliterator */
//基于索引的二分法,懒加载的 Spliterator
static final class ArrayListSpliterator<E> implements Spliterator<E> {...}
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
int removeCount = 0;
//666,用了类似掩码的思想,这样就能避免多次移动数组了,实现O(n),很不错
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
final E element = (E) elementData[i];
if (filter.test(element)) {
//set:Sets the bit at the specified index to true.
removeSet.set(i);
removeCount++;
}
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
// shift surviving elements left over the spaces left by removed elements
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
final int newSize = size - removeCount;
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
//nextClearBit:Returns the index of the first bit that is set to false
//false表示不移走,true表示移走
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
return anyToRemove;
}
public void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
elementData[i] = operator.apply((E) elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
public void sort(Comparator<? super E> c) {
final int expectedModCount = modCount;
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
}然后将这个
-GLOBAL__I_Hw
放进.o文件的一个.ctor
段中,最后由ld将各个.o文件的.ctor
段链接起来,并计算出全局对象数量填入crtbegin.o
即可。之后在
+_init
段中遍历.ctor
的各个函数指针进行构造函数调用就行了其中:
+
- +
Cloneable接口
与RandomAccess一样,都是一个规定性质的接口。
-+后日谈:今天又在rtt中看到了这一牛掰操作。rtt也是大概通过这个原理实现的帅的一匹的“Automatic Initialization Mechanism”。
-原理感觉也是将其放入一个特殊的”rti_fn$f”段,并且用rti_start和end来标识该段结束,
-+
// xiunian: INIT_EXPORT应该是这个
INIT_EXPORT(fn, "1.0") 宏展开:
const char __rti_level_fn[] = ".rti_fn." "1.0";
// 指示编译器将特定的变量或数据结构分配到名为 "rti_fn$f" 的内存段(Memory Segment)中
__declspec(allocate("rti_fn$f"))
rt_used const struct rt_init_desc __rt_init_msc_fn = {__rti_level_fn, fn };A class implements the Cloneable interface to indicate to the Object.clone() method that it is legal for that method to make a field-for-field copy of instances of that class.
+Classes that implement this interface should override Object.clone (which is protected) with a public method.
+- +
clone与浅拷贝(shallow copy)
-
public static void main(String[] args) {
ArrayList<Student> arr = new ArrayList<>();
arr.add(new Student("lylt",15));
ArrayList<Student> cl = (ArrayList)arr.clone();
System.out.println(cl.get(0).toString());
cl.get(0).name="Sam";
System.out.println(cl.get(0).toString());
System.out.println(arr.get(0).toString());
cl.set(0,new Student("HWX",19));
System.out.println(cl.get(0).toString());
System.out.println(arr.get(0).toString());
}
/*运行结果:
name :lyltage :15
name :Samage :15
name :Samage :15
name :HWXage :19
name :Samage :15
*/+
/*
* xiunian: 牛逼,这里颇有学链接时的感觉了
* 这里介绍了组件初始化顺序
* Components Initialization will initialize some driver and components as following
* order:
* rti_start --> 0
* BOARD_EXPORT --> 1
* rti_board_end --> 1.end
*
* DEVICE_EXPORT --> 2
* COMPONENT_EXPORT --> 3
* FS_EXPORT --> 4
* ENV_EXPORT --> 5
* APP_EXPORT --> 6
*
* rti_end --> 6.end
*
* These automatically initialization, the driver or component initial function must
* be defined with:
* INIT_BOARD_EXPORT(fn);
* INIT_DEVICE_EXPORT(fn);
* ...
* INIT_APP_EXPORT(fn);
* etc.
*/
static int rti_start(void)
{
return 0;
}
INIT_EXPORT(rti_start, "0");
static int rti_board_start(void)
{
return 0;
}
INIT_EXPORT(rti_board_start, "0.end");
static int rti_board_end(void)
{
return 0;
}
INIT_EXPORT(rti_board_end, "1.end");
static int rti_end(void)
{
return 0;
}
INIT_EXPORT(rti_end, "6.end");结合内部代码可知,确实跟上面那个toArray的原理是一样的,只拷贝地址。
+clone是浅拷贝。
+ +![]()
关于子类继承到的父类内部类
本来在犹豫,子类默认继承到的内部类里面用到的外部类方法的版本是取父还是取子,经过以下实验可知,是取能访问到的最新版本。
+-
public class Main {
public static void main(String[] args) {
ChildOuter chldot = new ChildOuter();
chldot.printname();
chldot.in.printall();
}
}
/*结果:子类声明为private的成员字段不能被从父类继承而来的方法访问到
Father
I am father!*/
class FatherOuter{
String name="Father";
private void print(){
System.out.println("I am father!");
}
public Inner in = new Inner();
public class Inner{
public int haha=1;
void printall(){
print();
}
}
public void printname(){
System.out.println(name);
}
}
class ChildOuter extends FatherOuter{
private String name="Child";
private void print(){System.out.println("I am child!");}
}之后真正初始化只需遍历然后调用函数指针即可
-
void rt_components_board_init(void)
{
volatile const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
{
(*fn_ptr)();
}
}- - 析构(
-_finit
)早期同理可得。现在变了,变成直接在
-GLOBAL__I_Hw
中注册atexit
了。
static void __tcf_1(void) //这个名字由编译器生成
{
Hw.~HelloWorld();
}实现小型运行库
-看到标题就知道接下来有多帅了
+如若把Father和Child内的print类都换成public:
++ +
class FatherOuter{
//...
public void print(){
System.out.println("I am father!");
}
//...
}
class ChildOuter extends FatherOuter{
//...
public void print(){System.out.println("I am child!");}
}
/*结果:访问最新版本
I am child!*/+
//结论:内部类可以访问外部类所有不论私有还是公有的资源;会优先访问最新版本【父子类而言】
//子类声明为private的成员字段不能被从父类继承而来的方法访问到,只会访问能访问到的最新版本序列化versionID
ArrayList实现了java.io.Serializable接口,故而可以被序列化和反序列化,就需要有个序列化版本ID
+What is a serialVersionUID and why should I use it?
++-这东西是用来在反序列化的时候保证收到的对象和发送的对象的类是相同的。If the receiver has loaded a class for the object that has a different
+serialVersionUID
than that of the corresponding sender’s class, then deserialization will result in anInvalidClassException
.The field serialVersionUID must be static, final, and of type
+long
.If a serializable class does not explicitly declare a
serialVersionUID
, then the serialization runtime will calculate a defaultserialVersionUID
value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification.在这一章我们仅实现CRT几个关键的部分。虽然这个迷你CRT仅仅实现了为数不多的功能,但是它已经具备了CRT的关键功能:入口函数、初始化、堆管理、基本IO,甚至还将实现堆C++的new/delete、stream和string的支持。
-本章主要分为两个部分,首先实现一个仅仅支持C语言的运行库,即传统意义上的CRT。其次,将为这个CRT加入一部分以支持C++语言的运行时特性。
-相关代码放在github了,其实感觉差不多是按它写的抄了一遍。可以现在稍微整理下文件结构。
--
-- -
just for C
-前面说到,CRT的作用是执行init和finit段、进行堆的管理、进行IO的封装管理以及提供各种标准C语言库。因而,我们可以分别用如下几个文件来实现这几个功能:
--
-- -
-
entry.c
用于实现入口函数
mini_crt_entry
。入口函数中主要要做:调用main之前的栈构造、堆初始化、IO初始化,最后调用main函数。main函数返回后,通过系统调用exit来杀死进程。-
malloc.c
用于实现堆的管理,主要实现了
+malloc
和free
。使用了空闲链表的小内存管理法,实现简单。- -
transient
+++transient是短暂的意思。对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。 因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。
+在持久化对象时,对于一些特殊的数据成员(如用户的密码,银行卡号等),我们不想用序列化机制来保存它。为了在一个特定对象的一个成员变量上关闭序列化,可以在这个成员变量前加上关键字transient。
+注意static修饰的静态变量天然就是不可序列化的。一个静态变量不管是否被transient修饰,均不能被序列化(如果反序列化后类中static变量还有值,则值为当前JVM中对应static变量的值)。序列化保存的是对象状态,静态变量保存的是类状态,因此序列化并不保存静态变量。
+使用场景
+
(1)类中的字段值可以根据其它字段推导出来,如一个长方形类有三个属性长度、宽度、面积,面积不需要序列化。
(2) 一些安全性的信息,一般情况下是不能离开JVM的。
(3)如果类中使用了Logger实例,那么Logger实例也是不需要序列化的但其实还有一个问题,它这边源码对这个transient的注释是:
+non-private to simplify nested class access
,“非私有以简化嵌套类访问”。问题是这个transient和类的公有还是私有有什么关系呢?Why is the data array in java.util.ArrayList package-private?
+其实没怎么看懂23333
-
stdio.c
用于实现IO封装,
+fread
、fwrite
、fopen
、fclose
、fseek
。实现简单,因而只是系统调用的封装- -
ArrayList的elementData虽然被transient修饰,但仍然能够序列化
-
string.c
以字符串操作为例,提供的标准C语言库。
+- -
关于static的空数组
EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA这两个空数组可用于表示两种情况:
+new ArrayList(0) ->EMPTY_ELEMENTDATA
+new ArrayList() ->DEFAULTCAPACITY_EMPTY_ELEMENTDATA
+之所以用静态,是提取了共性:不论是需要什么ArrayList,其空形态不都一样吗(
+这样可以避免了制造对象的浪费。very good。
之后,我们将其以如下参数编译为静态库:
-- -
$ gcc -c -fno-builtin -nostdlib -fno-stack-protector entry.c malloc.c stdio.c string.c printf.c
$ ar -rs minicrt.a malloc.o printf.o stdio.o string.o
# 编译测试用例
$ gcc -m32 -c -ggdb -fno-builtin -nostdlib -fno-stack-protector test.c再指定
-mini_crt_entry
为入口进行静态链接:
$ ld -m elf_i386 -static -e mini_crt_entry entry.o test.o minicrt.a -o testC++
-如果要实现对C++的支持,除了在上述基础上,我们还需增加以下几个内容:全局对象(cout)构造/析构的实现、new/delete、类的实现(string和iostream)。具体来说,会支持下面这个简单的代码:
-+
using namespace std;
int main(int argc, char* argv[])
{
string* msg = new string("Hello World");
cout << *msg << endl;
delete msg;
return 0;
}- +
关于扩容的连环计
我其实觉得不必写那么麻烦……
+ +经过测试替换可得,确实可以像我那样写。
+
//此处的ArrayList是魔改过的
//now the capacity is 0
ArrayList a = new ArrayList<>();
System.out.println(a.getCapacity());
//扩容到DEFAULT
a.add(new Student("Lily",15));
System.out.println(a.getCapacity());
addStudent(a);
System.out.println(a.getCapacity());
/*运行结果:
0
10
33
*/关于clear我的写法
+-
ArrayList a = new ArrayList<>();
addStudent(a);
long sum1=0;
for (int i=0;i<50000;i++){
long startTime = System.currentTimeMillis();
a.clear1();
addStudent(a);
long endTime = System.currentTimeMillis();
sum1+=endTime-startTime;
}
//sum1/=500;
System.out.println("clear1: "+sum1);
sum1=0;
for (int i=0;i<50000;i++){
long startTime = System.currentTimeMillis();
a.clear2();
addStudent(a);
long endTime = System.currentTimeMillis();
sum1+=endTime-startTime;
}
//sum1/=500;
System.out.println("clear2: "+sum1);我们可以分步实现这些功能:
--
+- -
new/delete实现
-简单地使用运算符重载功能即可:
-
void* operator new(unsigned int size);
void operator delete(void* p);- -
类的实现
-不多说
+经测试发现不分伯仲()也确实差距应该很小2333
+不过依照一个回答:
+++Your approach explicitly throws the backing array away. The existing implementation attempts to reuse it. So even if your approach is faster in isolation, in practice it will almost certainly be less performant. Since calling
+clear()
is a sign you intend to reuse theArrayList
.其实感觉我的clear说不定花销更大,毕竟要创建一个新对象(
全局对象的构造/析构
--
+构造
-全局对象的构造在entry中进行:
-+
void mini_crt_entry(void)
{
...
// 构造所有全局对象
do_global_ctors();
ret = main(argc,argv);
}- -
heap write traffic
What is heap write traffic and why it is required in ArrayList?
+详见第二个答案。
+-
前文说过,在Linux中,每个.o文件的全局构造最后都会放在
-.ctor
段。ld在链接阶段中将所有目标文件(包括用于标识.ctor
段开始和结束的crtbegin.o
和crtend.o
)的.ctor
段连在一起。所以,我们就需要实现三个文件:-
+- -
-
ctors.c
主要是用于实现
-do_global_ctors()
。既然都有.ctor
段存在了,那么它的实现就很简单,就是遍历.ctor
段的所有函数指针并且调用它们。+
void run_hooks();
extern "C" void do_global_ctors()
{
run_hooks();
}栈放在一级缓存,堆放在二级缓存
+总之意思就是成员变量写在堆里,局部变量写在栈里,做
+-
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
// Update cursor while iterating
cursor = i;
}
void run_hooks()
{
const ctor_func *list = ctors_begin;
// 逐个调用ctors段里的东西
while ((int)*++list != -1) (**list)();
}- -
-
crtbegin.c
前文说到,按规定,ld将会以如下顺序连接.o文件:
-+
ld crtbegin.o 其他文件 crtend.o -o test比做
+-
while (cursor != size && modCount == expectedModCount) {
consumer.accept((E) elementData[cursor++]);
}因而,
-crtbegin.c
的.ctor
段会被链接在第一个。其作用是标识.ctor
函数指针的数量,将在链接时由ld计算并且填写。因而在这里,我们只需将其初始化为一个特殊值(-1)就行:
typedef void (*ctor_func)(void);
ctor_func ctors_begin[1] __attribute__((section(".ctors"))) = {
(ctor_func)-1
};- -
-
crtend.c
同样,
-crtend.c
的.ctor
段标识着.ctor
段的结束。因而我们也将其初始化为一个特殊值(-1):
typedef void (*ctor_func)(void);
// 转化-1为函数指针,标识结束
ctor_func crt_end[1] __attribute__((section(".ctors"))) = {
(ctor_func) - 1
};花销更小
析构
-全局对象的析构同样在entry中进行:
-+
void mini_crt_entry(void)
{
...
ret = main(argc,argv);
exit(ret);
}
void exit(int exitCode)
{
// 执行atexit,完成所有finit钩子
mini_crt_call_exit_routine();
// 调用exit系统调用
asm( "movl %0,%%ebx \n\t"
"movl $1,%%eax \n\t"
"int $0x80 \n\t"
"hlt \n\t"::"m"(exitCode));
}- -
if (i >= elementData.length)
is i >= elementData.length in ArrayList::iterator redundant?
+-
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}具体也是以链表形式管理所有的函数指针,在
+atexit
中注册(加入链表),在mini_crt_call_exit_routine
中真正调用,不多分析。如果 user invoke trimToSize method ,就会导致在
checkForComodification();
和if (i >= elementData.length)
之间发生ArrayIndexOutOfBounds
。而在if (i >= elementData.length)
之后trim没有影响,因为我们的局部变量已经保存了原来的elementData,此时再trim只是修改成员变量的elementData,局部变量依然不变。- -
sublist不可序列化,且not cloneable
+ +
private class SubList extends AbstractList<E> implements RandomAccesssublist没有extends Cloneable, java.io.Serializable这两个接口
parent和ArrayList.this
首先,这两个是同一个吗?其次,这俩是否是同一个跟sub的级数有关系吗,就比如一级sub都是同一个,多级sub就不是同一个了?
+经过对ArrayList的一些public和以下代码的测试,得出结论:这两个只有在第一级sub的时候是同一个。parent指向直系父亲,ArrayList.this指向root父亲。
++ +
//In ArrayList:
public final AbstractList<E> parent;
public ArrayList getRoot(){return ArrayList.this;}
//In test main:
public static void main(String[] args) {
ArrayList a=new ArrayList();
addStudent(a);
System.out.println(a.hashCode());
ArrayList.SubList suba= (ArrayList.SubList) a.subList(1,7);
System.out.println(suba.getRoot().hashCode()+"\t"+suba.parent.hashCode());
System.out.println(suba.hashCode());
ArrayList.SubList subsuba=(ArrayList.SubList) suba.subList(2,4);
System.out.println(subsuba.getRoot().hashCode()+"\t"+subsuba.parent.hashCode());
System.out.println(subsuba.hashCode());
ArrayList.SubList subsubsuba=(ArrayList.SubList) subsuba.subList(0,1);
System.out.println(subsubsuba.getRoot().hashCode()+"\t"+subsubsuba.parent.hashCode());
}
/*输出结果:
779301330
779301330 779301330
-954172011
779301330 -954172011
-95519366
779301330 -95519366
*/总之,ArrayList的sublist实现方式相当于串成了一条父子继承串,多级sub,至于这么干相比原来的只有两级父子关系的方法好在哪就不知道了
特辑:开发中遇到的链接小问题
-
已经在
-LD_LIBRARY_PATH
中加入某个静态库的路径,但是仍然报错error while loading shared libraries: libssl.so.1.1: cannot open shared object file: No such file or directory
解决方法:执行
-sudo ldconfig
--
ldconfig
命令的作用就是将这些共享库的路径添加到动态链接器的缓存中,以便系统能够找到它们。具体而言,
-ldconfig
会检查默认的共享库路径(通常是/lib
和/usr/lib
),以及在/etc/ld.so.conf
和/etc/ld.so.conf.d/
目录中定义的其他路径。然后,它会更新动态链接器缓存,这样系统就知道在哪里查找共享库。Q:也就是说ld不是像gcc找头文件,是根据-I选项规定的路径即时查找的,而是只根据缓存吗?所以尽管我们通过ld path环境变量设置了新查找路径,我们还是得手动刷新下ld缓存。
-A:是的,你理解得很对。
-ldconfig
主要是更新系统中的共享库缓存,而不是在每次程序运行时实时查找库文件的位置。动态链接器在运行时会根据缓存中的信息找到所需的共享库,以提高性能并避免在每次程序启动时重新搜索所有库。当你通过
+LD_LIBRARY_PATH
环境变量设置新的查找路径时,这只是告诉动态链接器在运行时应该额外搜索这些路径。然而,为了确保系统广泛地认识这些新的路径,以及其中的共享库,你需要运行ldconfig
来更新缓存。AbstractSequentialList(A)
++提供顺序访问list的基本骨架
+To implement a list the programmer needs only to extend this class and provide implementations for the listIterator and size methods. For an unmodifiable list, the programmer need only implement the list iterator’s hasNext, next, hasPrevious, previous and index methods.
For a modifiable list the programmer should additionally implement the list iterator’s set method. For a variable-size list the programmer should additionally implement the list iterator’s remove and add methods.代码:
+ +
public abstract class AbstractSequentialList<E> extends AbstractList<E> {
protected AbstractSequentialList() {
}
public E get(int index) {
try {
//1
return listIterator(index).next();
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public E set(int index, E element) {
try {
ListIterator<E> e = listIterator(index);
E oldVal = e.next();
e.set(element);
return oldVal;
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public void add(int index, E element) {
try {
listIterator(index).add(element);
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public E remove(int index) {
try {
ListIterator<E> e = listIterator(index);
E outCast = e.next();
e.remove();
return outCast;
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
// Bulk Operations
public boolean addAll(int index, Collection<? extends E> c) {
try {
boolean modified = false;
ListIterator<E> e1 = listIterator(index);
Iterator<? extends E> e2 = c.iterator();
while (e2.hasNext()) {
e1.add(e2.next());
modified = true;
}
return modified;
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
// Iterators
public Iterator<E> iterator() {
return listIterator();
}
public abstract ListIterator<E> listIterator(int index);
}其中:
-]]>
idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/
typora 替换图片asset
- \!\[.*\]\(D:\\aWorkStorage\\hexo\\blog\\source\\_posts\\阅读JDK容器部分源码的心得体会1【Collection部分】\\(.*)\.png\)
,
替换结果{% asset_img $1.png %}
+-双向链表,实现List和Deque
+并发不安全。List list = Collections.synchronizedList(new LinkedList(…));
+印象:漂亮的指针操作,以及好像很少抛出异常,还有很多很多繁琐的方法(
public class LinkedList<E> |
/*实现这个接口的类可用于for-each循环*/ |
In the clear()
function:
// Clearing all of the links between nodes is "unnecessary", but: |
还没看懂,插个眼
+一切都反过来了,也没有升序迭代器恁多方法:
+不支持foreach循环,只支持单向遍历,没有add set 只有remove。
+事实证明确实人家也觉得无参比较合理(
+LinkedList用了从AbstractList继承来的sublist相关类和方法,没有特别的优化,其sublist不可序列化,且not cloneable。
+In Java LinkedList, why the “first”, “last” and “size” variable are transient?
--引入迭代器的目的是为了**统一**“对容器里的元素进行遍历”这一操作。
+LinkedList provide its own method for serializing and de-serializing.
+When serializing, it only writes the size, and the values of the list.
+When deserializing, it reads the size, then build the list from scratch, each node for each value at a time.
+If the author did not provide their own read and write methods, then they would need to make size, first, and last non-transient. They would also need to make the Node class serializable.
public interface Iterator<E> { |
--注意,iterator并不指向具体元素,它指向的是元素的间隙
-这些^就是iterator所指的位置。这样就能理解iterator的next和previous了吧2333
-- -
而set()所修改的元素,是其上一次调用next()或者previous()方法所返回的元素。
-What’s the meaning of this source code of interface Collection in JAVA?
+Otherwise serializing would be by default, which would be recursive, and for a large list would easily blow the stack.
public interface ListIterator<E> extends Iterator<E> { |
-Note that the remove and set(Object) methods are not defined in terms of the cursor position; they are defined to operate on the last element returned by a call to next or previous().
+意思就是序列化时不记录这些信息,反序列化时会重新构建。还有说如果用默认的序列化方法是递归的可能爆栈?还有我觉得有一点可能是如果把所有node都序列化了,可能反序列化后,本来分配到那段内存空间要是被占用了,但指针值不变还是会有问题?等待之后解答。
+
+-Unlike the new collection implementations, Vector is synchronized. If a thread-safe implementation is not needed, it is recommended to use ArrayList in place of Vector.
/* |
public class Vector<E> |
Set是不允许重复的元素集合的ADT,【ADT:抽象数据结构】
-Bag是元素集合的ADT,允许重复.
-通常,任何包含元素的东西都是Collection.
-任何允许重复的集合都是Bag,否则就是Set.
-通过索引访问元素的任何包都是List.
-在最后一个之后附加新元素并且具有从头部(第一索引)移除元素的方法的Bag是Queue.
-在最后一个之后附加新元素并且具有从尾部(最后一个索引)移除元素的方法的Bag是Stack.
————————————————
原文链接:https://blog.csdn.net/weixin_34239718/article/details/114036886
空构造器Vector()创建出来的默认容量为10,不同于ArrayList是个空集合。
这回答里写得很清楚:What are destructive and non-destructive methods in java?
+与ArrayList基本上是雷同的,就是都是synchronized。
--Some collection operations which perform recursive traversal of the collection may fail with an exception for self-referential instances where the collection directly or indirectly contains itself.
-
Java 8 vs Java 7 Collection Interface: self-referential instance
-这个的第二个回答【较长的那个】写得很棒,较短的那个似乎是错误的。
-正如描述所说的,“directly or indirectly contains itself”,回答里那个例子正是因为“indirectly contains itself”。
-不仅仅是集合,每个Object都可能出现这样的错误(因为都包含有toString)
-但是注意一点:
-ArrayList l1 = new ArrayList(); |
这段代码是正常的,是因为ArrayList里面toString的实现:
-sb.append('['); |
实现中只是把东西设置为空,并没有trim,因而容量不变
+/* 1 |
规避了这种风险。
-下面的hashcode是不正常的,因为hashcode实现没有规避这种风险。
+需要规避可修改对象,使其与集合中另一个元素重复的问题
+详见此:
+Java HashSet contains duplicates if contained element is modified
++The correct solution is to stick to the contract of
+Set
and not modify objects after adding them to the collection.You can avoid this problem by either:
++
+- using an immutable type for your set elements,
+- making a copy of the objects as you put them into the set and / or pull them out of the set,
+- writing your code so that it “knows“ not to change the objects for the duration …
+From the perspective of correctness and robustness, the first option is clearly best.
+
For toArray() : |
set抽象自数学的集合,因此有很多对应的集合操作:
+++addAll ∪
+retainAll ∩
+removeAll -
+
++Note that this class does not override any of the implementations from the AbstractCollection class. It merely adds implementations for equals and hashCode.
+
public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> { |
List<String> list = Arrays.asList("foo", "bar", "baz"); |
++In particular, it does not guarantee that the order will remain constant over time.
+Iterating over this set requires time proportional to the sum of the HashSet instance’s size (the number of elements) plus the “capacity” of the backing HashMap instance (the number of buckets).
+Thus, it’s very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.
+This implementation is not synchronized.
+Set s = Collections.synchronizedSet(new HashSet(...));
public class HashSet<E> |
ArrayList<Student> a = new ArrayList<>(); |
正如它的解释:
+++Dummy value to associate with an Object in the backing Map
+
set 以map作为内部支持,其实主要用的是map对于key的高效去重。也就是说,set其实只需要用map的key这一半就好了。所以我们另一半value就都统一用一个new Object【也就是PRESENT】来统一就行。
+不得不说这点很聪明,值得学习。
+private static final Object PRESENT = new Object(); |
++A Set that further provides a total ordering on its elements.
+The set’s iterator will traverse the set in ascending element order.
+All elements inserted into a sorted set must implement the Comparable interface (or be accepted by the specified comparator).否则导致ClassCastException
+Note that the ordering maintained by a sorted set (whether or not an explicit comparator is provided) must be consistent with equals if the sorted set is to correctly implement the Set interface.
+【consistent with equals:if and only if c.compare(e1, e2)==0 has the same boolean value as e1.equals(e2) for every e1 and e2 in S.】
+//2
+Note: several methods return subsets with restricted ranges. 区间是前闭后开的。
+If you need a closed range , and the element type allows for calculation of the successor of a given value, merely request the subrange from lowEndpoint to successor(highEndpoint).
+For example, suppose that s is a sorted set of strings. [low,hight]
+
SortedSet<String> sub = s.subSet(low, high+"\0");
A similar technique can be used to generate an open range (low,hight)
SortedSet<String> sub = s.subSet(low+"\0", high);
66666
+
public interface SortedSet<E> extends Set<E> { |
ArrayList<Student> a = new ArrayList<>(); |
只有sorted set才有subset,想想也确实
+/* |
这也就说明,toArray()实际上是把list的元素复制一份弄成array,直接把值粘贴进去。
-对于引用对象,list的元素实际上应该存的是对象在堆中的地址。所谓的“安全”指的是,修改array中的元素的值【也即对象地址】,也就是换一个气球牵,是不会影响原来list的元素的值的。
-因而,对于样例1和2,我们其实给array的元素换了个气球牵,或者是把list换了个气球牵,相互对象不同,没什么影响。
-对于样例3,我们修改了list和array共同指向的对象【就像C语言的指针那样】
-以上参考自What does “Safe” mean in the Collections.toArray() JavaDoc?
+以及注意此处是Element,不是Index
starkoverflow关于为什么要设立default的讨论:
-What is the purpose of the default keyword in Java
-Default methods were added to Java 8 primarily to support lambda expressions.
为什么加个”\0”就可以,具体可以看看这个:
+Adding “\0” to a subset range end
+原因就是sub的这个range取的是在此区间的元素,low和high这两个param不一定要包含在这个set里面。因此,按照set的排序,high+”\0”比high大,因而high就落入此区间,也就可以被包含在range中了。
-To implement an unmodifiable collection, the programmer needs only to extend this class and provide implementations for the iterator and size methods.
+
To implement a modifiable collection, the programmer must additionally[也要搞上面的] override this class’s add method (which otherwise throws an UnsupportedOperationException), and the iterator returned by the iterator method must additionally implement its remove method.NavigableSet(I)
+-比起sorted set,navigable set最特殊的点在于它提供了对某一元素附近元素的导航。
+Method usage
+lower less than最大的,比所给ele小的元素
+floor less than or equal最大的,比所给ele小或者等于的元素
+ceiling greater than or equal最小的,比所给ele大或者等于的元素
+higher greater than最小的,比所给ele大的元素
+The descendingSet method returns a view of the set with the senses of all relational and directional methods inverted.
+This interface additionally defines methods pollFirst and pollLast that return and remove the lowest and highest element, if one exists, else returning null. 有点堆的感觉
+Methods subSet, headSet, and tailSet differ from the like-named SortedSet methods in accepting additional arguments describing whether lower and upper bounds are inclusive versus exclusive.
- -
public abstract class AbstractCollection<E> implements Collection<E> {
//唯一的构造函数。 (用于子类构造函数的调用,通常是隐式的。)
protected AbstractCollection() {
}
// 查询操作
public abstract Iterator<E> iterator();
public abstract int size();
public boolean isEmpty() {
return size() == 0;
}
public boolean contains(Object o) {
Iterator<E> it = iterator();
if (o==null) {
while (it.hasNext())
if (it.next()==null)
return true;
} else {
while (it.hasNext())
if (o.equals(it.next()))
return true;
}
return false;
}
public Object[] toArray() {
// Estimate size of array; be prepared to see more or fewer elements
Object[] r = new Object[size()];
Iterator<E> it = iterator();
for (int i = 0; i < r.length; i++) {
if (! it.hasNext()) // fewer elements than expected
return Arrays.copyOf(r, i);
r[i] = it.next();
}
return it.hasNext() ? finishToArray(r, it) : r;
}
public <T> T[] toArray(T[] a) {
// Estimate size of array; be prepared to see more or fewer elements
int size = size();
//通过反射得到a同类型的数组实例
T[] r = a.length >= size ? a :
(T[])java.lang.reflect.Array
.newInstance(a.getClass().getComponentType(), size);
Iterator<E> it = iterator();
for (int i = 0; i < r.length; i++) {
if (! it.hasNext()) { // fewer elements than expected
if (a == r) {
r[i] = null; // null-terminate用null来标记数组结束。但如果数组里也有null该怎么办?从前面的contains来看也是有可能的
} else if (a.length < i) {
return Arrays.copyOf(r, i);
} else {
System.arraycopy(r, 0, a, 0, i);
if (a.length > i) {
a[i] = null;
}
}
return a;
}
r[i] = (T)it.next();
}
// more elements than expected
return it.hasNext() ? finishToArray(r, it) : r;
}
/*
为啥要-8?
Some VMs reserve some header words in an array. Attempts to allocate larger arrays may result in OutOfMemoryError: Requested array size exceeds VM limit
好像是因为要给8个字节保留作为数组的头标题。
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//Reallocates the array being used within toArray when the iterator returned more elements than expected, and finishes filling it from the iterator.
private static <T> T[] finishToArray(T[] r, Iterator<?> it) {
int i = r.length;
while (it.hasNext()) {
int cap = r.length;
//说明此时需要扩容
if (i == cap) {
//每次扩当前大小的1/2
int newCap = cap + (cap >> 1) + 1;
// overflow-conscious code 溢出
if (newCap - MAX_ARRAY_SIZE > 0)
//cap+1 扩容后至少应该比原大小大1
newCap = hugeCapacity(cap + 1);
//申请一个船新数组空间
r = Arrays.copyOf(r, newCap);
}
r[i++] = (T)it.next();
}
// 如果过度扩容了就缩小回刚刚好
return (i == r.length) ? r : Arrays.copyOf(r, i);
}
private static int hugeCapacity(int minCapacity) {
//如果最小扩容也失败,说明要的东西太多了,救不了
if (minCapacity < 0) // overflow
throw new OutOfMemoryError
("Required array size too large");
//否则
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
//修改操作
public boolean add(E e) {
throw new UnsupportedOperationException();
}
public boolean remove(Object o) {
Iterator<E> it = iterator();
if (o==null) {
while (it.hasNext()) {
if (it.next()==null) {
it.remove();
return true;
}
}
} else {
while (it.hasNext()) {
if (o.equals(it.next())) {
it.remove();
return true;
}
}
}
return false;
}
//批量操作
//O(n^2)
public boolean containsAll(Collection<?> c) {
for (Object e : c)
if (!contains(e))
return false;
return true;
}
public boolean addAll(Collection<? extends E> c) {
boolean modified = false;
for (E e : c)
if (add(e))
modified = true;
return modified;
}
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<?> it = iterator();
while (it.hasNext()) {
if (c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
Iterator<E> it = iterator();
while (it.hasNext()) {
if (!c.contains(it.next())) {
it.remove();
modified = true;
}
}
return modified;
}
public void clear() {
Iterator<E> it = iterator();
while (it.hasNext()) {
it.next();
it.remove();
}
}
//字符串操作
public String toString() {
Iterator<E> it = iterator();
if (! it.hasNext())
return "[]";
StringBuilder sb = new StringBuilder();
sb.append('[');
for (;;) {
E e = it.next();
sb.append(e == this ? "(this Collection)" : e);
if (! it.hasNext())
return sb.append(']').toString();
sb.append(',').append(' ');
}
}
}其中:
--
-- -
在toArray方法中,为什么需要写这么奇怪的代码?
what’s the usage of the code in the implementation of AbstractCollection’s toArray Method
-
Yes, you're right, as the javadoc sais, this method is prepared to return correctlly even if the Collection has been modified in the mean time.【并发安全】 That's why the initial size is just a hint. The usage of the iterator also ensures avoidance from the "concurrent modification" exception.Queue
Queue(I)
-+
代码:
-
public interface NavigableSet<E> extends SortedSet<E> {
//Returns the greatest element in this set strictly less than the given element
E lower(E e);
E floor(E e);
E ceiling(E e);
E higher(E e);
//Removes the first (lowest) element, or returns null if this set is empty.
E pollFirst();
E pollLast();
//in ascending order
Iterator<E> iterator();
/*
The returned set has an ordering equivalent to
Collections.reverseOrder(comparator()).
The expression s.descendingSet().descendingSet()
returns a view of s essentially equivalent(基本等价) to s
*/
NavigableSet<E> descendingSet();
//Equivalent in effect to descendingSet().iterator()
Iterator<E> descendingIterator();
NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
E toElement, boolean toInclusive);
NavigableSet<E> headSet(E toElement, boolean inclusive);
NavigableSet<E> tailSet(E fromElement, boolean inclusive);
SortedSet<E> subSet(E fromElement, E toElement);
SortedSet<E> headSet(E toElement);
SortedSet<E> tailSet(E fromElement);
}The Queue interface does not define the blocking queue methods, which are common in concurrent programming. These methods, which wait for elements to appear or for space to become available, are defined in the java.util.concurrent.BlockingQueue interface, which extends this interface.
-Queue implementations generally do not define element-based versions of methods equals and hashCode【就是不会像之前的list一样遍历一遍通过单个元素的hashcode计算整体的hashcode】 but instead inherit the identity based versions from class Object【hashcode由对象决定】, because element-based equality is not always well-defined for queues with the same elements but different ordering properties.
-Each of these methods exists in two forms: one throws an exception if the operation fails, the other returns a special value (either null or false, depending on the operation). 容量受限的队列推荐使用第二种form
+TreeSet
+-This implementation provides guaranteed log(n) time cost for the basic operations (add, remove and contains).
+Note that this implementation is not synchronized.
+
SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));
代码:
- -
public interface Queue<E> extends Collection<E> {
/*
The offer method inserts an element if possible, otherwise returning false.
不同于Collection的add方法,offer添加失败时不会抛出异常,而是直接return false
*/
boolean add(E e);
boolean offer(E e);
/*
The remove() and poll() methods differ only in their behavior
when the queue is empty:
the remove() method throws an exception, while the poll() method returns null.
*/
E remove();
E poll();
/*
The element() and peek() methods return,
but do not remove, the head of the queue.
*/
E element();
E peek();
}Deque(I)
-双端队列。
-The name deque is short for “double ended queue” and is usually pronounced “deck”.
-- -
This interface provides two methods to remove interior elements, removeFirstOccurrence and removeLastOccurrence.
-+
代码:
-
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
//依然用了个map
private transient NavigableMap<E,Object> m;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
public TreeSet() {
this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
public Iterator<E> iterator() {
return m.navigableKeySet().iterator();
}
public Iterator<E> descendingIterator() {
return m.descendingKeySet().iterator();
}
//确实直接让map倒序就可以了
public NavigableSet<E> descendingSet() {
return new TreeSet<>(m.descendingMap());
}
public int size() {
return m.size();
}
public boolean isEmpty() {
return m.isEmpty();
}
public boolean contains(Object o) {
return m.containsKey(o);
}
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return m.remove(o)==PRESENT;
}
public void clear() {
m.clear();
}
public boolean addAll(Collection<? extends E> c) {
// Use linear-time version if applicable
if (m.size()==0 && c.size() > 0 &&
c instanceof SortedSet &&
m instanceof TreeMap) {
SortedSet<? extends E> set = (SortedSet<? extends E>) c;
TreeMap<E,Object> map = (TreeMap<E, Object>) m;
Comparator<?> cc = set.comparator();
Comparator<? super E> mc = map.comparator();
if (cc==mc || (cc != null && cc.equals(mc))) {
map.addAllForTreeSet(set, PRESENT);
return true;
}
}
return super.addAll(c);
}
public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
E toElement, boolean toInclusive) {
return new TreeSet<>(m.subMap(fromElement, fromInclusive,
toElement, toInclusive));
}
public NavigableSet<E> headSet(E toElement, boolean inclusive) {
return new TreeSet<>(m.headMap(toElement, inclusive));
}
public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
return new TreeSet<>(m.tailMap(fromElement, inclusive));
}
public SortedSet<E> subSet(E fromElement, E toElement) {
return subSet(fromElement, true, toElement, false);
}
public SortedSet<E> headSet(E toElement) {
return headSet(toElement, false);
}
//注意此处为true。严格遵循左闭右开
public SortedSet<E> tailSet(E fromElement) {
return tailSet(fromElement, true);
}
public Comparator<? super E> comparator() {
return m.comparator();
}
public E first() {
return m.firstKey();
}
public E last() {
return m.lastKey();
}
// NavigableSet API methods
public E lower(E e) {
return m.lowerKey(e);
}
public E floor(E e) {
return m.floorKey(e);
}
public E ceiling(E e) {
return m.ceilingKey(e);
}
public E higher(E e) {
return m.higherKey(e);
}
public E pollFirst() {
Map.Entry<E,?> e = m.pollFirstEntry();
return (e == null) ? null : e.getKey();
}
public E pollLast() {
Map.Entry<E,?> e = m.pollLastEntry();
return (e == null) ? null : e.getKey();
}
public Object clone() {
TreeSet<E> clone;
try {
clone = (TreeSet<E>) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
clone.m = new TreeMap<>(m);
return clone;
}
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {...}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {...}
public Spliterator<E> spliterator() {
return TreeMap.keySpliteratorFor(m);
}
private static final long serialVersionUID = -2479143000061671589L;
}+]]>
idea 替换注释正则表达式/\*{1,2}[\s\S]*?\*/
typora 替换图片asset
+\!\[.*\]\(D:\\aWorkStorage\\hexo\\blog\\source\\_posts\\阅读JDK容器部分源码的心得体会2【Map部分】\\(.*)\.png\)
,
替换结果{% asset_img $1.png %}
++A map cannot contain duplicate keys; each key can map to at most one value.
+This interface takes the place of the Dictionary class.
+The Map interface provides three collection views, which allow a map’s contents to be viewed as a set of keys, collection of values, or set of key-value mappings.
+The order of a map is defined as the order in which the iterators on the map’s collection views return their elements. map的元素顺序取决于集合元素顺序的意思?
+Note: great care must be exercised if mutable objects are used as map keys. The behavior of a map is not specified if the value of an object is changed in a manner that affects equals comparisons while the object is a key in the map. 【这个跟set的那个是一样的】
+
map没有迭代器
+public interface Map<K,V> { |
public interface Deque<E> extends Queue<E> { |
get return null when value==NULL or key不存在。
+为了区分这两种情况,写代码时可以用:
+if !containsKey(key){ |
-Resizable-array implementation of the Deque interface.
-不允许空
-Array deques have no capacity restrictions; they grow as necessary to support usage.
-not thread-safe【相比于由vector实现的线程安全的Stack】
-This class is likely to be faster than Stack when used as a stack, and faster than LinkedList when used as a queue.【6】
-fail-fast
+其实源码中的getordefault方法就给出了应用典范
+
default V getOrDefault(Object key, V defaultValue) {
V v;
return (((v = get(key)) != null) || containsKey(key))
? v
: defaultValue;
}
+-//Returns a Set view of the keys contained in this map.
+//The set is backed by the map, so changes to the map are reflected in the set, and vice-versa.
public class ArrayDeque<E> extends AbstractCollection<E> |
如下代码测试:
+public static void main(String[] args) { |
空构造器的默认容量为16
-是一个便捷的截位取余操作,这跟hashmap一个原理,详见hashmap第二点。
-delete方法返回true,说明右移数组,此时next指针需要++
-delete方法返回false,说明左移数组,此时next指针不变
-跟差不多所有的迭代器实现一样,此方法执行完毕之后,cursor直接跳到数组最末,相当于迭代结束
+可得,与之前的List一样,这个view都是纯粹基于原数组的,实时变化的。
+在应用中可发现,可以通过map的key和value的set来对map进行遍历。
有序、支持随机访问
-/* |
Note: While it is permissible for lists to contain themselves as elements, extreme caution is advised: the equals and hashCode methods are no longer well defined on such a list.【这跟上面的引用点4是差不多的。】
-ArrayList l1 = new ArrayList(); |
+++
//The map can be modified while an iteration over the set is in progress
//when using the setValue operation on a map entry returned by the iterator
//or through the iterator's own remove operation
相比于其它的view,多了第二句话
sublist的document有一个很有趣的点,就是把sublist称为原list的视图view,这不禁让人想起了数据库里的表和表的视图。
-但是还是有差别的。
-在数据库中,视图仅仅是表或表的一部分的快照,修改视图对原表没有影响。但此处,对sublist的结构性修改和非结构性修改都会使原list的对应元素发生改变。
-但有一点是相同的。如果对原表/原list修改,那么视图就会没用/会寄掉。
-可以像这样来对主list指定范围内的元素进行操作,免去复杂的下标。
-//For example, the following idiom removes a range of elements from a list: |
public class Main { |
+If the function returns null no mapping is recorded.
+If the function itself throws an (unchecked) exception, the exception is rethrown, and no mapping is recorded.
+The most common usage is to construct a new object serving as an initial mapped value or memoized result, as in:
+-
map.computeIfAbsent(key, k -> new Value(f(k)));为什么之前的toArray得到的array换了个对象,原list不会变【详见Collection说明第五点】;而这里的sublist得到的子list换了个对象,原list也会变呢?
-也许这跟间接寻址的级数有关?之后看过ArrayList的具体实现再来解答。
---穿越回来:
- toArray得到array,是新开辟了存储空间,里面存放了原list对象的地址。因而,修改新存储空间的地址内容,原list是不变的。
- 但sublist连新开辟存储空间也没有,其差不多所有操作都是从list进行的。因而它换了个对象,具体实现就是让原list也换了个对象。
- 所以说其实前者更像是数据库里面的“视图”概念。
+Or to implement a multi-value map, Map<K,Collection
+>, supporting multiple values per key:
map.computeIfAbsent(key, k -> new HashSet<V>()).add(v);③ 关于sublis和原list、non-structral和structural的区别:
Can structural changes in sublist reflected in the original list, in JAVA?
+它这说的还是很抽象的,下面给出一个使用computeIfAbsent的优雅实例:
+TreeMap()) Treemap With Object
--In the document of List class:
-- -
/*
The returned list is backed by this list, so non-structural changes in the returned list are reflected in this list, and vice-versa.
*/
List<E> subList(int fromIndex, int toIndex);Can structural changes in sublist reflected in the original list in JAVA?
+
computeIfAbsent
returns the value that is already present in the map, or otherwise evaluates the lambda and puts that into the map, and returns that value.答案是会改变的。实例可看问题中代码,或者point3的代码。
-但是既然都会变的话,为什么文档里面要特意强调“non-structural”呢?这是因为,对sublist进行结构性改变会让原list也正常地一起变,但是对原list进行结构性改变却会让sublist寄掉:
-文档下面接着写道:
-+
The semantics of the list returned by this method become undefined if the backing list (i.e., this list) is structurally modified in any way other than via the returned list. (Structural modifications are those that change the size of this list, or otherwise perturb it in such a fashion that iterations in progress may yield incorrect results.)
/*
sublist会寄,如果其原list被结构性改变了,且是以除了通过sublist结构性改变的方式外的其他方式改变的。(结构性改变是指那些会改变list的长度,或者会使迭代失败的那些改变)
*/-
var line = "line";
var mp = new TreeMap<String,TreeMap<String,Integer>>();
var m = mp.computeIfAbsent(line, k -> new TreeMap<>());
m.put("content", 5);
System.out.println(mp);
//output:{line={content=5}}通过代码验证可得,确实寄了
-+
public static void main(String[] args) {
ArrayList<Student> a=new ArrayList<>();
a.add(new Student("Lily",16));
a.add(new Student("Sam",16));
a.add(new Student("Tom",16));
a.add(new Student("Mary",16));
a.add(new Student("Mark",16));
a.add(new Student("John",16));
List<Student> suba=a.subList(2,5);
System.out.println("Firstly,suba:");
printall(suba);
printall(a);
System.out.println("Change the origin structure:");
a.remove(3);
System.out.println("a:");
printall(a);
System.out.println("suba:");
printall(suba);
}
/*运行结果
Firstly,suba:
name :Tomage :16 name :Maryage :16 name :Markage :16
name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Maryage :16 name :Markage :16 name :Johnage :16
Change the origin structure:
a:
name :Lilyage :16 name :Samage :16 name :Tomage :16 name :Markage :16 name :Johnage :16
suba:
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.ArrayList$SubList.checkForComodification(ArrayList.java:1241)
*/computIfAbsent发现此时map里面没有这个“line”key,就执行第二个参数的lambda表达式,把一个new TreeMap<>以line为关键字放入,并且返回该TreeMap。
+
+看起来非常实用:
+This method may be of use when combining multiple mapped values for a key【相同key不同value合并】. For example, to either create or append a String msg to a value mapping:
+-
map.merge(key, msg, String::concat)通过以上可以感觉,估计sublist和原list的元素是共享存储空间的,只不过可能对象里有相关维护的变量。
-Any method accessing the list through the sub list effectively does index + offset.
故而要是list变了,sublist的相关维护变量不变,就会依然傻傻地进行offset+index操作,这样就会寄。此猜想有待验证23333-穿越回来:
--
+- 确实是共享存储空间的,不如说sublist就直接引用了原list的变量,所有操作实质上都是在原list上进行。
-- sublist傻傻地进行offset+index操作,这样体现在原list上,可能导致下标越界或者结果并非我们想要的。
-所举代码段意为把新值通过字符串拼接接在旧值后面。
+应该也可以用于集合合并。总之具体实现方法取决于传入的function参数,非常实用
map本身没有迭代器。
+因而在对map进行遍历时,只能通过其keyset、valueset以及entryset来实现。
+具体详见:HashMap的四种遍历方式
+-提供随机访问list的基本骨架
-To implement an unmodifiable list, the programmer needs only to extend this class and provide implementations for the get(int) and size() methods.
-To implement a modifiable list, the programmer must additionally override the set(int, Object) method
-If the list is variable-size,the programmer must additionally override the add(int, E) and remove(int) methods.
-Unlike the other abstract collection implementations, the programmer does not have to provide an iterator implementation; the iterator and list iterator are implemented by this class, on top of the “random access“ methods: get(int), set(int, E), add(int, E) and remove(int).这里的迭代器反而是通过类里面的方法来实现的
+AbstractMap
+-To implement an unmodifiable map, the programmer needs only to extend this class and provide an implementation for the entrySet method, which returns a set-view of the map’s mappings. Typically, the returned set will, in turn, be implemented atop AbstractSet. This set should not support the add or remove methods, and its iterator should not support the remove method.
+To implement a modifiable map, the programmer must additionally override this class’s put method (which otherwise throws an UnsupportedOperationException), and the iterator returned by entrySet().iterator() must additionally implement its remove method.
代码
+
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
protected AbstractList() {
}
//重写add方法
public boolean add(E e) {
//此为下面的public void add(int index, E element);
add(size(), e);
return true;
}
//交给具体实现
abstract public E get(int index);
//假设不能修改
public E set(int index, E element) {
throw new UnsupportedOperationException();
}
public void add(int index, E element) {
throw new UnsupportedOperationException();
}
public E remove(int index) {
throw new UnsupportedOperationException();
}
// Search Operations
public int indexOf(Object o) {
ListIterator<E> it = listIterator();
if (o==null) {
while (it.hasNext())
if (it.next()==null)
return it.previousIndex();
} else {
while (it.hasNext())
if (o.equals(it.next()))
return it.previousIndex();
}
return -1;
}
public int lastIndexOf(Object o) {
//从后往前遍历
ListIterator<E> it = listIterator(size());
if (o==null) {
while (it.hasPrevious())
if (it.previous()==null)
return it.nextIndex();
} else {
while (it.hasPrevious())
if (o.equals(it.previous()))
return it.nextIndex();
}
return -1;
}
// Bulk Operations
public void clear() {
removeRange(0, size());
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
boolean modified = false;
for (E e : c) {
add(index++, e);
modified = true;
}
return modified;
}
// Iterators
public Iterator<E> iterator() {
return new Itr();
}
public ListIterator<E> listIterator() {
return listIterator(0);
}
public ListIterator<E> listIterator(final int index) {
rangeCheckForAdd(index);
return new ListItr(index);
}
//内部类诶 1
private class Itr implements Iterator<E> {
//Index of element to be returned by subsequent call to next.
int cursor = 0;
//Index of element returned by most recent call to next or previous. Reset to -1 if this element is deleted by a call to remove.
int lastRet = -1;
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size();
}
public E next() {
checkForComodification();
try {
int i = cursor;
E next = get(i);
lastRet = i;
cursor = i + 1;
return next;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.remove(lastRet);
if (lastRet < cursor)
cursor--;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException e) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0;
}
public E previous() {
checkForComodification();
try {
int i = cursor - 1;
E previous = get(i);
//lastRet=i;cursor=i;
lastRet = cursor = i;
return previous;
} catch (IndexOutOfBoundsException e) {
checkForComodification();
throw new NoSuchElementException();
}
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor-1;
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
AbstractList.this.set(lastRet, e);
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
AbstractList.this.add(i, e);
lastRet = -1;
cursor = i + 1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
//迭代器定义结束
public List<E> subList(int fromIndex, int toIndex) {
//Sublist和RandomAccessSubList在后面都作为内部类定义
//这俩的分支主要是能否支持高性能随机访问,而这点在Java是依靠是否实现RandomAccess接口
//来体现的,要实现接口必须得是一个类。因此,为了分歧,这里不得不创建两个类来表示两种情况。
//这两个类的方法应该是大致相同的。
//2
return (this instanceof RandomAccess ?
new RandomAccessSubList<>(this, fromIndex, toIndex) :
new SubList<>(this, fromIndex, toIndex));
}
// Comparison and hashing
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof List))
return false;
ListIterator<E> e1 = listIterator();
ListIterator<?> e2 = ((List<?>) o).listIterator();
while (e1.hasNext() && e2.hasNext()) {
E o1 = e1.next();
Object o2 = e2.next();
//如果这东西没有重载equals方法,那此处就是单纯object对象是否相同了
if (!(o1==null ? o2==null : o1.equals(o2)))
return false;
}
//为啥size不一样不在一开始就比呢?那样不是更省花销吗
return !(e1.hasNext() || e2.hasNext());
}
public int hashCode() {
int hashCode = 1;
for (E e : this)
hashCode = 31*hashCode + (e==null ? 0 : e.hashCode());
return hashCode;
}
protected void removeRange(int fromIndex, int toIndex) {
ListIterator<E> it = listIterator(fromIndex);
for (int i=0, n=toIndex-fromIndex; i<n; i++) {
it.next();
it.remove();
}
}
//The number of times this list has been structurally modified.
//3
protected transient int modCount = 0;
private void rangeCheckForAdd(int index) {
if (index < 0 || index > size())
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size();
}
}
class SubList<E> extends AbstractList<E> {
//这大概是对父list的引用
private final AbstractList<E> l;
//这大概是在父list的起始偏移量
private final int offset;
//sublist的长度
private int size;
SubList(AbstractList<E> list, int fromIndex, int toIndex) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > list.size())
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
l = list;
offset = fromIndex;
size = toIndex - fromIndex;
this.modCount = l.modCount;
}
public E set(int index, E element) {
rangeCheck(index);
checkForComodification();
//改变sublist也会改变父list,是否成功取决于对父list的改变是否成功
return l.set(index+offset, element);
}
public E get(int index) {
rangeCheck(index);
checkForComodification();
//直接取父类值
return l.get(index+offset);
}
public int size() {
checkForComodification();
return size;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
checkForComodification();
l.add(index+offset, element);
this.modCount = l.modCount;
size++;
}
public E remove(int index) {
rangeCheck(index);
checkForComodification();
E result = l.remove(index+offset);
this.modCount = l.modCount;
size--;
return result;
}
protected void removeRange(int fromIndex, int toIndex) {
checkForComodification();
l.removeRange(fromIndex+offset, toIndex+offset);
this.modCount = l.modCount;
size -= (toIndex-fromIndex);
}
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
int cSize = c.size();
if (cSize==0)
return false;
checkForComodification();
l.addAll(offset+index, c);
this.modCount = l.modCount;
size += cSize;
return true;
}
public Iterator<E> iterator() {
return listIterator();
}
public ListIterator<E> listIterator(final int index) {
checkForComodification();
rangeCheckForAdd(index);
//改变迭代器实现
return new ListIterator<E>() {
//子类迭代器=父类迭代器+offset
private final ListIterator<E> i = l.listIterator(index+offset);
public boolean hasNext() {
return nextIndex() < size;
}
public E next() {
if (hasNext())
return i.next();
else
throw new NoSuchElementException();
}
public boolean hasPrevious() {
return previousIndex() >= 0;
}
public E previous() {
if (hasPrevious())
return i.previous();
else
throw new NoSuchElementException();
}
public int nextIndex() {
return i.nextIndex() - offset;
}
public int previousIndex() {
return i.previousIndex() - offset;
}
public void remove() {
i.remove();
SubList.this.modCount = l.modCount;
size--;
}
public void set(E e) {
i.set(e);
}
public void add(E e) {
i.add(e);
SubList.this.modCount = l.modCount;
size++;
}
};
}
//层层套娃啊
public List<E> subList(int fromIndex, int toIndex) {
return new SubList<>(this, fromIndex, toIndex);
}
private void rangeCheck(int index) {
if (index < 0 || index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private void rangeCheckForAdd(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
private void checkForComodification() {
if (this.modCount != l.modCount)
throw new ConcurrentModificationException();
}
}
//确实实现上没什么区别,主要是多了个RandomAccess的约定。
class RandomAccessSubList<E> extends SubList<E> implements RandomAccess {
RandomAccessSubList(AbstractList<E> list, int fromIndex, int toIndex) {
super(list, fromIndex, toIndex);
}
public List<E> subList(int fromIndex, int toIndex) {
return new RandomAccessSubList<>(this, fromIndex, toIndex);
}
}-
其中:
-
内部类ListItr和Itr的实现
Itr的实现需要用到AbstratcList类的get和set方法,而显然不同Collection的get和set不一样。为了避免混淆,Itr就只能作为私有类。为了避免胡乱引用,Itr就可以直接作为内部类,共享其外部类的所有资源。
-ListItr作为内部私有类很容易理解,毕竟只有list才需要它。
+最核心的还是entrySet。其余所有的方法,都是通过enrtSet实现的。而给定了enrty这个数据结构的实现方式,剩下的就是entrySet具体怎么实现了。AbstractMap把entrySet的实现抽象了出来,交给了其实现类去具体实现。
+代码:
+ +
public abstract class AbstractMap<K,V> implements Map<K,V> {
protected AbstractMap() {
}
// Query Operations
public int size() {
//还真确实是set的大小
return entrySet().size();
}
public boolean isEmpty() {
return size() == 0;
}
public boolean containsValue(Object value) {
Iterator<Entry<K,V>> i = entrySet().iterator();
if (value==null) {
while (i.hasNext()) {
//entrySet的元素是Entry
Entry<K,V> e = i.next();
if (e.getValue()==null)
return true;
}
} else {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (value.equals(e.getValue()))
return true;
}
}
return false;
}
public boolean containsKey(Object key) {
Iterator<Map.Entry<K,V>> i = entrySet().iterator();
if (key==null) {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (e.getKey()==null)
return true;
}
} else {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (key.equals(e.getKey()))
return true;
}
}
return false;
}
public V get(Object key) {
Iterator<Entry<K,V>> i = entrySet().iterator();
if (key==null) {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (e.getKey()==null)
return e.getValue();
}
} else {
while (i.hasNext()) {
Entry<K,V> e = i.next();
if (key.equals(e.getKey()))
return e.getValue();
}
}
return null;
}
// Modification Operations
public V put(K key, V value) {
throw new UnsupportedOperationException();
}
//为啥unmodifiable map还可以remove
public V remove(Object key) {
Iterator<Entry<K,V>> i = entrySet().iterator();
Entry<K,V> correctEntry = null;
if (key==null) {
while (correctEntry==null && i.hasNext()) {
Entry<K,V> e = i.next();
if (e.getKey()==null)
correctEntry = e;
}
} else {
while (correctEntry==null && i.hasNext()) {
Entry<K,V> e = i.next();
if (key.equals(e.getKey()))
correctEntry = e;
}
}
V oldValue = null;
if (correctEntry !=null) {
oldValue = correctEntry.getValue();
i.remove();
}
return oldValue;
}
// Bulk Operations
public void putAll(Map<? extends K, ? extends V> m) {
for (Map.Entry<? extends K, ? extends V> e : m.entrySet())
put(e.getKey(), e.getValue());
}
public void clear() {
//还真是
entrySet().clear();
}
// Views
//1
transient Set<K> keySet;
transient Collection<V> values;
//The set supports element removal via
//the Iterator.remove, Set.remove, removeAll, retainAll, and clear operations.
//It does not support the add or addAll operations.
//只删不加
public Set<K> keySet() {
//引用成员变量,减少访问堆次数
Set<K> ks = keySet;
//首次建立视图
if (ks == null) {
ks = new AbstractSet<K>() {
public Iterator<K> iterator() {
return new Iterator<K>() {
private Iterator<Entry<K,V>> i = entrySet().iterator();
public boolean hasNext() {
return i.hasNext();
}
public K next() {
return i.next().getKey();
}
public void remove() {
i.remove();
}
};
}
//AbtractMap的这些方法都是通过其entryset实现的。因此其实最主要的还是entryset怎么实现的
public int size() {
return AbstractMap.this.size();
}
public boolean isEmpty() {
return AbstractMap.this.isEmpty();
}
public void clear() {
AbstractMap.this.clear();
}
public boolean contains(Object k) {
return AbstractMap.this.containsKey(k);
}
};
//赋值回给成员变量
keySet = ks;
}
return ks;
}
public Collection<V> values() {
Collection<V> vals = values;
if (vals == null) {
vals = new AbstractCollection<V>() {
public Iterator<V> iterator() {
return new Iterator<V>() {
private Iterator<Entry<K,V>> i = entrySet().iterator();
public boolean hasNext() {
return i.hasNext();
}
public V next() {
return i.next().getValue();
}
public void remove() {
i.remove();
}
};
}
public int size() {
return AbstractMap.this.size();
}
public boolean isEmpty() {
return AbstractMap.this.isEmpty();
}
public void clear() {
AbstractMap.this.clear();
}
public boolean contains(Object v) {
return AbstractMap.this.containsValue(v);
}
};
values = vals;
}
return vals;
}
//有待不同的数据结构实现了
public abstract Set<Entry<K,V>> entrySet();
// Comparison and hashing
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Map))
return false;
Map<?,?> m = (Map<?,?>) o;
if (m.size() != size())
return false;
try {
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext()) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
if (value == null) {
if (!(m.get(key)==null && m.containsKey(key)))
return false;
} else {
if (!value.equals(m.get(key)))
return false;
}
}
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
return true;
}
public int hashCode() {
int h = 0;
Iterator<Entry<K,V>> i = entrySet().iterator();
while (i.hasNext())
h += i.next().hashCode();
return h;
}
public String toString() {
Iterator<Entry<K,V>> i = entrySet().iterator();
if (! i.hasNext())
return "{}";
StringBuilder sb = new StringBuilder();
sb.append('{');
for (;;) {
Entry<K,V> e = i.next();
K key = e.getKey();
V value = e.getValue();
//经典防自环
sb.append(key == this ? "(this Map)" : key);
sb.append('=');
sb.append(value == this ? "(this Map)" : value);
if (! i.hasNext())
return sb.append('}').toString();
sb.append(',').append(' ');
}
}
protected Object clone() throws CloneNotSupportedException {
AbstractMap<?,?> result = (AbstractMap<?,?>)super.clone();
//也就只有这两个成员变量了
result.keySet = null;
result.values = null;
return result;
}
private static boolean eq(Object o1, Object o2) {
return o1 == null ? o2 == null : o1.equals(o2);
}
// Implementation Note: SimpleEntry and SimpleImmutableEntry
// are distinct unrelated classes, even though they share
// some code. Since you can't add or subtract final-ness
// of a field in a subclass, they can't share representations,
// and the amount of duplicated code is too small to warrant
// exposing a common abstract class.
//意思就是说,这两个类一个表示key不可变value可变的entry,也就是可变map,
//另一个表示key和value都不可变的entry,也就是固定map,
//这俩有很多重复代码,但不能统一到一起,是因为前者有一个final字段,后者有两个,
//无法对这个final字段做一个统一,因此只能分成两个了
//静态内部类
//对Entry接口的一个简单实现【key不可变,value可变】
public static class SimpleEntry<K,V>
implements Entry<K,V>, java.io.Serializable
{
private static final long serialVersionUID = -8499721149061103585L;
//key不可修改,value可修改
private final K key;
private V value;
public SimpleEntry(K key, V value) {
this.key = key;
this.value = value;
}
public SimpleEntry(Entry<? extends K, ? extends V> entry) {
this.key = entry.getKey();
this.value = entry.getValue();
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V value) {
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return eq(key, e.getKey()) && eq(value, e.getValue());
}
public int hashCode() {
//注意这里是异或
return (key == null ? 0 : key.hashCode()) ^
(value == null ? 0 : value.hashCode());
}
public String toString() {
return key + "=" + value;
}
}
//静态内部类
//对Entry接口的一个简单实现【key不可变,value不可变】
public static class SimpleImmutableEntry<K,V>
implements Entry<K,V>, java.io.Serializable
{
private static final long serialVersionUID = 7138329143949025153L;
private final K key;
private final V value;
public SimpleImmutableEntry(K key, V value) {
this.key = key;
this.value = value;
}
public SimpleImmutableEntry(Entry<? extends K, ? extends V> entry) {
this.key = entry.getKey();
this.value = entry.getValue();
}
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V value) {
//exception
throw new UnsupportedOperationException();
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return eq(key, e.getKey()) && eq(value, e.getValue());
}
public int hashCode() {
return (key == null ? 0 : key.hashCode()) ^
(value == null ? 0 : value.hashCode());
}
public String toString() {
return key + "=" + value;
}
}
}其中:
+
+- -
view
++Each of these fields are initialized to contain an instance of the appropriate view the first time this view is requested. The views are stateless, so there’s no reason to create more than one of each.
+不同于之前List的sublist和sorted set的subset,它俩是调用创建view方法时才构造出一个新的对象,map是直接把values和keys视图放入成员变量了,因为Collection的视图从实用角度来说有起始和终点更实用,map不需要这个性质,因此作为成员变量花费更小
- -
RandomAccess
RandomAccess是一个空接口,它应该代表一个约定俗成的规定,即它的implementations的随机访问都是性能较高的。这个空接口思想很常见,源码带给我们的智慧。
-
按经验来说, a List implementation should implement this interface, if this loop:
for (int i=0, n=list.size(); i < n; i++)
list.get(i);
runs faster than this loop:
for (Iterator i=list.iterator(); i.hasNext(); )
i.next();关于modCount字段与fail-fast机制
-modCount字段就是保证一定程度并发安全的变量,fail-fast就是指马上抛出异常。
-+
HashMap
哈希表+链表/红黑树
+++permits null values and the null key允许空,其hash应该是0
+The HashMap class is roughly equivalent to Hashtable, except that it is unsynchronized and permits nulls.不同步
+This class makes no guarantees as to the order of the map; in particular, it does not guarantee that the order will remain constant over time.无序
+这应该差不多就是个桶链表
+An instance of HashMap has two parameters that affect its performance: initial capacity and load factor.
+The capacity is the number of buckets in the hash table.桶数量=capacity
+The load factor is a measure of how full the hash table is allowed to get before its capacity is automatically increased. 如果装载百分比达到load factor,hashmap的capacity就会自动增长。
+When the number of entries in the hash table exceeds the product of the load factor and the current capacity, the hash table is rehashed.如果元素数量>=load factor*capacity,就会自动增长并且重新hash。
+默认的load factor是0.75.【我其实觉得这个数很有意思。它是二进制意义上的整除数,因而计算应该很方便:它可以被整整表示,并且计算时可以拆成“2^-1+2^-2”以供移位简化】
+我们设置capacity和load factor的意图应该是要尽量减少rehash的次数。
+Note that using many keys with the same hashCode() is a sure way to slow down performance of any hash table使用多个相同的key【指hashcode相同】会降低性能【?】
+https://stackoverflow.com/questions/43911369/hashmap-java-8-implementation等会看看
+总之意思差不多就是,hashmap的数据结构:
+table数组,每个成员都是一个桶,桶里面装着结点。table默认长度为16
+每个桶内结点的结构依具体情况(该桶内元素多少)来决定,桶内元素多则用树状结构,少就用简单的线性表结构。线性结构为Node<K,V>,树状结构为TreeNode<K,V>。
+当一个线性表桶内结点多于临界值,就需要进行树化,会从链表变成红黑树;当整个hashmap结点数多于临界值,就需要增长capacity并且进行rehash。
+hashmap的桶的装配:首先通过key的hashcode算出一个hash值,然后再把该hash值与n-1相与就能得到桶编号。接下来再在桶内找到应插入的结点就行。
+代码:
-
public class HashMap<K,V> extends AbstractMap<K,V>
implements Map<K,V>, Cloneable, Serializable {
private static final long serialVersionUID = 362498820763181265L;
/*
此映射通常充当分箱(分桶)哈希表,但当箱变得太大时,它们会转换为 TreeNode 的箱,
每个结构类似于 java.util.TreeMap 中的结构。
大多数方法尝试使用正常的 bin,但出于实用性有时候会过渡到 TreeNode 方法(只需检查节点的实例)。
TreeNode 的 bin 可以像任何其他 bin 一样被遍历和使用,但在填充过多时还支持更快的查找。
但是,由于绝大多数正常使用的 bin 并没有过度填充,
因此在 table 方法的过程中检查树 bin 的存在可能会白花时间。
因为 TreeNode 的大小大约是常规节点的两倍,
所以我们仅在 bin 包含足够的节点以保证使用时才使用它们(请参阅 TREEIFY_THRESHOLD)。
当它们变得太小(由于移除或调整大小)时,它们会被转换回plain bins。
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
static final int MAXIMUM_CAPACITY = 1 << 30;
static final float DEFAULT_LOAD_FACTOR = 0.75f;
/*
The bin count 临界值 for using a tree rather than list for a bin.
当桶内节点数大于等于该值时,桶将由链表连接转化为树状结构。
该值必须大于 2 并且应该至少为 8,以便与树移除中关于在收缩时转换回普通 bin 的假设相吻合。
*/
static final int TREEIFY_THRESHOLD = 8;
//The bin count threshold for untreeifying a (split) bin during a resize operation.
static final int UNTREEIFY_THRESHOLD = 6;
/*
The smallest table capacity for which bins may be treeified.
(Otherwise the table is resized if too many nodes in a bin.)
Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts between
resizing and treeification thresholds.
*/
static final int MIN_TREEIFY_CAPACITY = 64;
static class Node<K,V> implements Map.Entry<K,V> {
//一旦被构造器初始化,就不可变。
final int hash;
//结点的键不变,但值可变
final K key;
V value;
//链表结构
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
//也就是说它自己的hashcode和构造时给它的hash是不一样的
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
/* ----------------静态共用方法-------------- */
//hash的计算方法
//1
static final int hash(Object key) {
int h;
//逻辑右移
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
//3
static Class<?> comparableClassFor(Object x) {
if (x instanceof Comparable) {
Class<?> c; Type[] ts, as; Type t; ParameterizedType p;
if ((c = x.getClass()) == String.class) // bypass checks
return c;
//检查所有接口
if ((ts = c.getGenericInterfaces()) != null) {
for (int i = 0; i < ts.length; ++i) {
if (((t = ts[i]) instanceof ParameterizedType) &&
((p = (ParameterizedType)t).getRawType() ==
Comparable.class) &&
(as = p.getActualTypeArguments()) != null &&
as.length == 1 && as[0] == c) // type arg is c
return c;
}
}
}
return null;
}
// for cast to Comparable
static int compareComparables(Class<?> kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 :
//会调用最新版本的方法
((Comparable)k).compareTo(x));
}
//这一通操作可以得到比cap大的,且离cap最近的2的幂次方数
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
/* ---------------- Fields -------------- */
/*
The table, initialized on first use, and resized as necessary.
长度是2的幂次或者0【初始】
*/
transient Node<K,V>[] table;
//4
transient Set<Map.Entry<K,V>> entrySet;
//初始为0,每put一次元素就++。
transient int size;
transient int modCount;
//达到此值时hashmap需要增长capacity并且rehash
// (可序列化
// Additionally, if the table array has not been allocated, this
// field holds the initial array capacity, or zero signifying
// DEFAULT_INITIAL_CAPACITY.)
int threshold;
final float loadFactor;
/* ---------------- Public operations -------------- */
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
public HashMap() {
this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
public HashMap(Map<? extends K, ? extends V> m) {
this.loadFactor = DEFAULT_LOAD_FACTOR;
putMapEntries(m, false);
}
//Implements Map.putAll and 上面的Map constructor的辅助方法
//evict – false when initially constructing this map, else true
final void putMapEntries(Map<? extends K, ? extends V> m, boolean evict) {
int s = m.size();
if (s > 0) {
if (table == null) { // pre-size
//+1保证了至少比m大
float ft = ((float)s / loadFactor) + 1.0F;
int t = ((ft < (float)MAXIMUM_CAPACITY) ?
(int)ft : MAXIMUM_CAPACITY);
if (t > threshold)
threshold = tableSizeFor(t);
//延迟resize,随处可见的懒汉思想,很聪明
}
else if (s > threshold)
//就地resize
resize();
for (Map.Entry<? extends K, ? extends V> e : m.entrySet()) {
K key = e.getKey();
V value = e.getValue();
putVal(hash(key), key, value, false, evict);
}
}
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K,V> getNode(int hash, Object key) {
Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
if ((tab = table) != null && (n = tab.length) > 0 &&
(first = tab[(n - 1) & hash]) != null) {
if (first.hash == hash && // always check first node
((k = first.key) == key || (key != null && key.equals(k))))
return first;
if ((e = first.next) != null) {
if (first instanceof TreeNode)
return ((TreeNode<K,V>)first).getTreeNode(hash, key);
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
return e;
} while ((e = e.next) != null);
}
}
return null;
}
public boolean containsKey(Object key) {
return getNode(hash(key), key) != null;
}
//put方法的实现
public V put(K key, V value) {
//计算key的哈希值
return putVal(hash(key), key, value, false, true);
}
//evict – false when initially constructing this map, else true
//Implements Map.put and related methods.
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
if ((tab = table) == null || (n = tab.length) == 0)
//此处调用resize初始化
n = (tab = resize()).length;
//n为table大小
//首先先找到所在桶
//如果所在桶不存在,就直接申请一个新桶(结点)放
//2此处找桶的方式
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
//所在桶存在
else {
//e为要塞进去value的结点,k为临时变量,用于存储key值
Node<K,V> e; K k;
//如果p的哈希值为key的哈希值,并且p的key==key,说明键本来就存在,并且正好是桶内第一个元素,只需修改旧键值对的value就行
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
//e=旧结点
e = p;
//否则需要沿着桶的结构继续往下找,这时候就需要看桶内用的是树状结构还是顺序结构了
//如果此时用的是树状结构
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
//用的是顺序结构
else {
for (int binCount = 0; ; ++binCount) {
//走到桶尽头,此时e==NULL
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//到达临界点,需要树化
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
//一直走,直到找到
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
//两个指针来回交替往下走
p = e;
}
}
//上面可以看到,只有原来就存在键值对才会满足此条件
if (e != null) { // existing mapping for key
V oldValue = e.value;
//onlyIfAbsent – if true, don't change existing value 除非旧值为空
if (!onlyIfAbsent || oldValue == null)
e.value = value;
//空操作,方便LinkedHashMap的后续实现
afterNodeAccess(e);
//存在旧键值对的情况至此结束
return oldValue;
}
}
//走到这说明是新建了一个结点
++modCount;
if (++size > threshold)
resize();
//空操作,方便LinkedHashMap的后续实现
afterNodeInsertion(evict);
return null;
}
//Initializes or doubles table size.
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
//决定newCap和newThr
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//扩容两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
//因为此时capacity已经需要向threshold转变了,因而newThr需要再计算
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
//需要复制原oldTab中的每个结点
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
//该桶只有一个结点
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
//5
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
//树化桶
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//如果表的一个桶结点数大于8(TREEIFY_THRESHOLD),但是表的总结点数小于64(MIN_TREEIFY_CAPACITY)也是不会树化的,只会resize重新hash
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
//需要树化
//取得该桶的头结点e
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
//replacementTreeNode return new TreeNode<>(p.hash, p.key, p.value, next);
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
//此时有0个结点
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
//只树化该桶
hd.treeify(tab);
}
}
//对于重复键需替换
public void putAll(Map<? extends K, ? extends V> m) {
putMapEntries(m, true);
}
//Returns:the previous value
public V remove(Object key) {
Node<K,V> e;
return (e = removeNode(hash(key), key, null, false, true)) == null ?
null : e.value;
}
//matchValue – if true only remove if value is equal
//value – the value to match if matchValue, else ignored
//movable – if false do not move other nodes while removing用于树
final Node<K,V> removeNode(int hash, Object key, Object value,
boolean matchValue, boolean movable) {
Node<K,V>[] tab; Node<K,V> p; int n, index;
//table和键都存在
if ((tab = table) != null && (n = tab.length) > 0 &&
(p = tab[index = (n - 1) & hash]) != null) {
//node为要移走的结点
Node<K,V> node = null, e; K k; V v;
//检查头结点
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
node = p;
else if ((e = p.next) != null) {
if (p instanceof TreeNode)
node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
else {
do {
if (e.hash == hash &&
((k = e.key) == key ||
(key != null && key.equals(k)))) {
node = e;
break;
}
p = e;
} while ((e = e.next) != null);
}
}
//需要移走
if (node != null && (!matchValue || (v = node.value) == value ||
(value != null && value.equals(v)))) {
if (node instanceof TreeNode)
((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
//由上文可知,此时node==p==头结点
//能找到这个差异点也是真牛逼
else if (node == p)
tab[index] = node.next;
//此时p.next=node
else
p.next = node.next;
++modCount;
--size;
afterNodeRemoval(node);
return node;
}
}
return null;
}
public void clear() {
Node<K,V>[] tab;
modCount++;
if ((tab = table) != null && size > 0) {
size = 0;
for (int i = 0; i < tab.length; ++i)
tab[i] = null;//我知道你要说什么:let GC do its work
}
}
//遍历。有树优化的话可以减少时间开销。
public boolean containsValue(Object value) {
Node<K,V>[] tab; V v;
if ((tab = table) != null && size > 0) {
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
if ((v = e.value) == value ||
(value != null && value.equals(v)))
return true;
}
}
}
return false;
}
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
//是HashMap自己实现的keyset
ks = new KeySet();
keySet = ks;
}
return ks;
}
final class KeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<K> iterator() { return new KeyIterator(); }
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return new KeySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super K> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new Values();
values = vs;
}
return vs;
}
final class Values extends AbstractCollection<V> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<V> iterator() { return new ValueIterator(); }
public final boolean contains(Object o) { return containsValue(o); }
public final Spliterator<V> spliterator() {
return new ValueSpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { HashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new EntryIterator();
}
//不如直接用map的contains、remove等等等
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
//只在值相等的时候remove
return removeNode(hash(key), key, value, true, true) != null;
}
return false;
}
public final Spliterator<Map.Entry<K,V>> spliterator() {
return new EntrySpliterator<>(HashMap.this, 0, -1, 0, 0);
}
public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
}
// Overrides of JDK8 Map extension methods
//Returns the value to which the specified key is mapped,
//or defaultValue if this map contains no mapping for the key.
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? defaultValue : e.value;
}
//If the specified key is not already associated with a value (or is mapped to null)
//associates it with the given value and returns null,
//else returns the current value.
public V putIfAbsent(K key, V value) {
return putVal(hash(key), key, value, true, true);
}
//只有在curVal==value且key存在的情况下才remove掉键值对
public boolean remove(Object key, Object value) {
return removeNode(hash(key), key, value, true, true) != null;
}
public boolean replace(K key, V oldValue, V newValue) {
Node<K,V> e; V v;
if ((e = getNode(hash(key), key)) != null &&
((v = e.value) == oldValue || (v != null && v.equals(oldValue)))) {
e.value = newValue;
afterNodeAccess(e);
return true;
}
return false;
}
public V replace(K key, V value) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) != null) {
V oldValue = e.value;
e.value = value;
afterNodeAccess(e);
return oldValue;
}
return null;
}
//如果key对应键值对不存在,就创建一个新的,并把它的值置为paramFunction(key)
//返回的是修改后的值。
//其他详见Map的第4点
public V computeIfAbsent(K key,
Function<? super K, ? extends V> mappingFunction) {
if (mappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
V oldValue;
if (old != null && (oldValue = old.value) != null) {
afterNodeAccess(old);
return oldValue;
}
}
V v = mappingFunction.apply(key);
if (v == null) {
return null;
} else if (old != null) {
old.value = v;
afterNodeAccess(old);
return v;
}
else if (t != null)
t.putTreeVal(this, tab, hash, key, v);
else {
tab[i] = newNode(hash, key, v, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
//++size后不用再check是否>threshold吗 ?为啥要交给上面一开始的时候判断
++size;
afterNodeInsertion(true);
return v;
}
//return 新值
public V computeIfPresent(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
Node<K,V> e; V oldValue;
int hash = hash(key);
if ((e = getNode(hash, key)) != null &&
(oldValue = e.value) != null) {
V v = remappingFunction.apply(key, oldValue);
if (v != null) {
e.value = v;
afterNodeAccess(e);
return v;
}
else
removeNode(hash, key, null, false, true);
}
return null;
}
public V compute(K key,
BiFunction<? super K, ? super V, ? extends V> remappingFunction) {
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
V oldValue = (old == null) ? null : old.value;
V v = remappingFunction.apply(key, oldValue);
if (old != null) {
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
removeNode(hash, key, null, false, true);
}
else if (v != null) {
if (t != null)
t.putTreeVal(this, tab, hash, key, v);
else {
tab[i] = newNode(hash, key, v, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return v;
}
public V merge(K key, V value,
BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
if (value == null)
throw new NullPointerException();
if (remappingFunction == null)
throw new NullPointerException();
int hash = hash(key);
Node<K,V>[] tab; Node<K,V> first; int n, i;
int binCount = 0;
TreeNode<K,V> t = null;
Node<K,V> old = null;
if (size > threshold || (tab = table) == null ||
(n = tab.length) == 0)
n = (tab = resize()).length;
if ((first = tab[i = (n - 1) & hash]) != null) {
if (first instanceof TreeNode)
old = (t = (TreeNode<K,V>)first).getTreeNode(hash, key);
else {
Node<K,V> e = first; K k;
do {
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k)))) {
old = e;
break;
}
++binCount;
} while ((e = e.next) != null);
}
}
if (old != null) {
V v;
if (old.value != null)
v = remappingFunction.apply(old.value, value);
else
v = value;
if (v != null) {
old.value = v;
afterNodeAccess(old);
}
else
removeNode(hash, key, null, false, true);
return v;
}
if (value != null) {
if (t != null)
t.putTreeVal(this, tab, hash, key, value);
else {
tab[i] = newNode(hash, key, value, first);
if (binCount >= TREEIFY_THRESHOLD - 1)
treeifyBin(tab, hash);
}
++modCount;
++size;
afterNodeInsertion(true);
}
return value;
}
public void forEach(BiConsumer<? super K, ? super V> action) {
Node<K,V>[] tab;
if (action == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next)
action.accept(e.key, e.value);
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
Node<K,V>[] tab;
if (function == null)
throw new NullPointerException();
if (size > 0 && (tab = table) != null) {
int mc = modCount;
for (int i = 0; i < tab.length; ++i) {
for (Node<K,V> e = tab[i]; e != null; e = e.next) {
e.value = function.apply(e.key, e.value);
}
}
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
// Cloning and serialization
public Object clone() {
HashMap<K,V> result;
try {
result = (HashMap<K,V>)super.clone();
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
result.reinitialize();
result.putMapEntries(this, false);
return result;
}
// These methods are also used when serializing HashSets
final float loadFactor() { return loadFactor; }
final int capacity() {
return (table != null) ? table.length :
(threshold > 0) ? threshold :
DEFAULT_INITIAL_CAPACITY;
}
private void writeObject(java.io.ObjectOutputStream s)
throws IOException {...}
private void readObject(ObjectInputStream s)
throws IOException, ClassNotFoundException {...}
// Support for resetting final field during deserializing
private static final class UnsafeHolder {...}
// iterators
//7
abstract class HashIterator {
Node<K,V> next; // next entry to return
Node<K,V> current; // current entry
int expectedModCount; // for fast-fail
int index; // current slot
HashIterator() {
expectedModCount = modCount;
Node<K,V>[] t = table;
current = next = null;
index = 0;
//指向第一个非空表项
if (t != null && size > 0) { // advance to first entry
do {} while (index < t.length && (next = t[index++]) == null);
}
}
public final boolean hasNext() {
return next != null;
}
final Node<K,V> nextNode() {
Node<K,V>[] t;
Node<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
//移动桶内指针
if ((next = (current = e).next) == null && (t = table) != null) {
//如果桶内表到达尽头,则移动选择桶的指针
do {} while (index < t.length && (next = t[index++]) == null);
}
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
final class KeyIterator extends HashIterator
implements Iterator<K> {
public final K next() { return nextNode().key; }
}
final class ValueIterator extends HashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
final class EntryIterator extends HashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
// spliterators
static class HashMapSpliterator<K,V> {...}
static final class KeySpliterator<K,V>
extends HashMapSpliterator<K,V>
implements Spliterator<K> {...}
static final class ValueSpliterator<K,V>
extends HashMapSpliterator<K,V>
implements Spliterator<V> {...}
static final class EntrySpliterator<K,V>
extends HashMapSpliterator<K,V>
implements Spliterator<Map.Entry<K,V>> {...}
// LinkedHashMap support
// Create a regular (non-tree) node
Node<K,V> newNode(int hash, K key, V value, Node<K,V> next) {
return new Node<>(hash, key, value, next);
}
// For conversion from TreeNodes to plain nodes
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
return new Node<>(p.hash, p.key, p.value, next);
}
// Create a tree bin node
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
return new TreeNode<>(hash, key, value, next);
}
// For treeifyBin
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
return new TreeNode<>(p.hash, p.key, p.value, next);
}
void reinitialize() {
table = null;
entrySet = null;
keySet = null;
values = null;
modCount = 0;
threshold = 0;
size = 0;
}
// Callbacks to allow LinkedHashMap post-actions
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
void afterNodeRemoval(Node<K,V> p) { }
// Called only from writeObject, to ensure compatible ordering.
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {...}
// Tree bins
//6红黑树介绍,此部分具体的红黑树实现省略
static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> {...}
}modCount不能保证绝对的并发安全,因为它只负责防范结构改变,而不负责看某位置的数据更新。
-在现实中要实现对集合的边迭代边修改,下面三种方式都是错的:
-+
其中:
+
+- +
hash()
-
第一种方法也可以把for里的size换成list.size()
-其中乐观锁与悲观锁可见:乐观锁
+hash=原hashcode^(原hashcode逻辑右移16位)
+这样的话,由于右移16位补零,此时高位的所有比特位都跟原来一样,低位的比特位变成了融合高低位特点的东西,这样就可以减少冲突,增加均匀性
+- +
table[(n-1)&hash]
具体看这个视频,讲得非常不错
+ +假设table此时为默认长度16.则n-1=15
+写出15的二进制形式:0000 1111,可以发现,任何数跟它相与,结果都一定为0000 xxxx,永不越界。
+写出16的二进制形式:0001 0000,可以发现,任何数跟它相与,结果都一定为16或者0.
+可以发现15有非常好的性质。
+而扩展出来,任何2的幂次方-1都具有这样的良好的性质。**这也是为什么hashmap要求表的长度应该为2的幂次。**
+而且,除了不会越界,还有一点就是,这个任何数与15相与的与操作就相当于,任何数对16取余的取余操作。这点实在是佩服啊,把复杂的取余操作在该场景下直接用一个位运算就搞定了。
+- +
comparableClassFor
树状结构时结点的默认排序方式是by hashCode。但如果两个结点元素之间是同一个class C,并且这个C实现了Comparable方法,那么就不会按照它们的hashCode比较,而是会调用class C的compareTo方法。
+(We conservatively(保守地) check generic types via reflection to validate(证实) this – see method comparableClassFor).
+也就是说这个comparableClassFor方法的意图就是,如果这个类是comparable的,就返回它具体类型,如果不是返回null。
+- +
entrySet
不同于AbstractMap中entrySet的核心作用,HashMap的put、get、clear等等等核心函数都不依赖于entrySet了,毕竟结构改变得比较多了。因而这里的entrySet字段保留,只是为了呼应AbstractMap中keyset和valueset的实现,以及补充AbstractMap中未给出的EntrySet实现。
+- +
resize()扩容旧表到新表的转移
此时需要复制oldTab中的所有结点。但注意,由于此时发生了扩容,hash的计算发生了变化,因而不能全部照搬不动oldTab中的下标,否则产生错误。因而我们需要了解一下如何调整下标。
+首先由代码可得,对于oldTab!=NULL的情况下,newCap一定是扩为原来的两倍的。因而以下只需讨论扩容为两倍的情况。
+由第2点可知,假设现在容量为16,扩容为原来的两倍,则hash掩码应该为0000 1111,扩容后,hash掩码应该为0001 1111,可见就只是多了一位,因而,oldTab中,若这一位的值为0,则在新表和旧表中位置的下标应该是一样的;若这一位的值为1,则新表下标=旧表下标+offset,offset正是等于0001 0000.而这个“0001 0000”,正是oldCap!
+对于容量为其他值,全部道理都是一样的。
+因而我们要做的,是对旧表的每一个桶内的所有结点,把它们分成两类,一类为(e.hash & oldCap) == 0【也就是这一位值为0 情况】和(e.hash & oldCap) == 1,然后对这两类进行在新表中分别映射即可。这段代码便做了这样的事。
+
//5
//low index head,下标保持不变
Node<K,V> loHead = null, loTail = null;
//high index head,下标需要增长偏移量
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
//第一类
if ((e.hash & oldCap) == 0) {
//一个简单的队列操作
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
//第二类
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
//对于第一类
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
//对于第二类
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;- +
红黑树
+这篇文章也写得很好:
+ +- +
HashIterator
注意点有二:
+①不继承Iterator接口
+②抽象,具体实现类为EntryIterator、KeyIterator和ValueIterator
+③map的接口定义是没有iterator的,因此map不能通过hashiterator迭代,只能通过其vie来实现【三个具体实现类】
+LinkedHashMap
哈希表+链表/红黑树+有序队列
-+-
+- 乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,只在更新的时候会判断一下在此期间别人有没有去更新这个数据。
-Hash table and linked list implementation of the Map interface, with predictable iteration order.
+This implementation differs from HashMap in that it maintains a doubly-linked list running through all of its entries.
+This linked list defines the iteration ordering, which is normally the order in which keys were inserted into the map (insertion-order).有序,顺序为元素插入的顺序
+Note that insertion order is not affected if a key is re-inserted into the map. 当修改key的value值时,key的插入序不变
+此实现既让hashmap变得有序,又不会像TreeMap一样有高成本。
+It can be used to produce a copy of a map that has the same order as the original, regardless of the original map’s implementation.
++ +
这样可以保持copymap的原有顺序
+A special constructor is provided to create a linked hash map whose order of iteration is the order in which its entries were last accessed, from least-recently accessed to most-recently (access-order). This kind of map is well-suited to building LRU caches. 可以有一个排序方式,顺序为最近最少访问->最近访问,这可以用来构建LRU cache【LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。】
+至于这个“access”怎么定义:
--注意“在此期间”的含义是拿到数据到更新数据的这段时间。因为没有加锁,所以别的线程可能会更改。还有一点那就是乐观锁其实是不加锁的来保证某个变量一系列操作原子性的一种方法。
+Invoking the put, putIfAbsent, get, getOrDefault, compute, computeIfAbsent, computeIfPresent, or merge methods results in an access to the corresponding entry (assuming it exists after the invocation completes). The replace methods only result in an access of the entry if the value is replaced. The putAll method generates one entry access for each mapping in the specified map, in the order that key-value mappings are provided by the specified map’s entry set iterator.
+注意没有remove
-
+- 悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞,直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。Java中
-synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。也因此,对map视图【各个set】的访问不算access。【因为不调用任意一个上面方法】
+可以重写 removeEldestEntry(Map.Entry) 方法,以在将新映射添加到映射时自动删除陈旧映射的策略。
++ + + +
//1
+Iteration over the collection-views of a LinkedHashMap requires time proportional to the size of the map, regardless of its capacity.不同于hashmap,迭代时间与容量无关。
+In access-ordered linked hash maps, merely querying the map with get is a structural modification.注意,对于access-ordered的lhm来说,**get也是一个structural modification,因为可能会修改排序顺序**。所以迭代时只能使用Iterator的next方法来得到结点,迭代器访问不会对accessorder有影响。
+代码测试:
+
LinkedHashMap<String,Integer> map = new LinkedHashMap<>(16,0.75f,true);
map.put("Lily",15);
map.put("Sam",20);
map.put("Mary",11);
map.put("Lee",111);
for(Iterator i = map.entrySet().iterator();i.hasNext();){
map.get("Lily");
System.out.println(i.next().toString());
}
/*
Exception in thread "main" java.util.ConcurrentModificationException
at java.util.LinkedHashMap$LinkedHashIterator.nextNode(LinkedHashMap.java:719)
*/总之意思就是,LinkedHashMap的数据结构:
+在HashMap哈希表+链表/红黑树的基础上,添加一个双端队列,该双端队列的作用是来维持内部的有序,因而开销比较大。应该只提供插入序和LRU序,其他需要用到compare的排序方法需要对某些方法(如afternodeXXX)进行重写,或者直接使用sorted map。
+LHM的一个很特殊的地方就是,它可以实现一个LRU这样的cache结构,只需要你重载removeEldestEntry return true。还可以在LHM的基础上实现有限长度map,只需要你重载removeEldestEntry 当元素>=某值时返回true。总而言之,你可以建造一个类在LHM的基础上,如果需要对map的长度有限制。
+LHM对LRU的实现是,一旦某个结点用到了,就立刻把他移到最队尾,然后每次淘汰淘汰队首。
+代码:
+ +
public class LinkedHashMap<K,V>
extends HashMap<K,V>
implements Map<K,V>
{
static class Entry<K,V> extends HashMap.Node<K,V> {
//原来只有next的
//双端队列
Entry<K,V> before, after;
Entry(int hash, K key, V value, Node<K,V> next) {
super(hash, key, value, next);
}
}
private static final long serialVersionUID = 3801124242820219131L;
//The head (eldest) of the doubly linked list.
transient LinkedHashMap.Entry<K,V> head;
//The tail (youngest) of the doubly linked list.
transient LinkedHashMap.Entry<K,V> tail;
//true:access顺序 false:插入顺序
final boolean accessOrder;
// internal utilities
// link at the end of list
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
LinkedHashMap.Entry<K,V> last = tail;
tail = p;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
}
// apply src's links to dst
//相当于用dst把src取代了
private void transferLinks(LinkedHashMap.Entry<K,V> src,
LinkedHashMap.Entry<K,V> dst) {
LinkedHashMap.Entry<K,V> b = dst.before = src.before;
LinkedHashMap.Entry<K,V> a = dst.after = src.after;
if (b == null)
head = dst;
else
b.after = dst;
if (a == null)
tail = dst;
else
a.before = dst;
}
// overrides of HashMap hook methods
void reinitialize() {
super.reinitialize();
head = tail = null;
}
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
LinkedHashMap.Entry<K,V> p =
new LinkedHashMap.Entry<K,V>(hash, key, value, e);
linkNodeLast(p);
return p;
}
Node<K,V> replacementNode(Node<K,V> p, Node<K,V> next) {
LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
LinkedHashMap.Entry<K,V> t =
new LinkedHashMap.Entry<K,V>(q.hash, q.key, q.value, next);
transferLinks(q, t);
return t;
}
TreeNode<K,V> newTreeNode(int hash, K key, V value, Node<K,V> next) {
TreeNode<K,V> p = new TreeNode<K,V>(hash, key, value, next);
linkNodeLast(p);
return p;
}
TreeNode<K,V> replacementTreeNode(Node<K,V> p, Node<K,V> next) {
LinkedHashMap.Entry<K,V> q = (LinkedHashMap.Entry<K,V>)p;
TreeNode<K,V> t = new TreeNode<K,V>(q.hash, q.key, q.value, next);
transferLinks(q, t);
return t;
}
//用于reove结点之后,之所以要存在就是因为LHM和HM的Node结构不一样,前者多了after和before
void afterNodeRemoval(Node<K,V> e) { // unlink
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.before = p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a == null)
tail = b;
else
a.before = b;
}
//调用于put、各种compute、merge
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> first;
//head是最老的结点
//如果需要插入新节点同时移去旧结点
if (evict && (first = head) != null && removeEldestEntry(first)) {
K key = first.key;
removeNode(hash(key), key, null, false, true);
}
}
void afterNodeAccess(Node<K,V> e) { // move node to last把用到的结点移到队尾
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
p.after = null;
if (b == null)
head = a;
else
b.after = a;
if (a != null)
a.before = b;
else
last = b;
if (last == null)
head = p;
else {
p.before = last;
last.after = p;
}
tail = p;
++modCount;
}
}
void internalWriteEntries(java.io.ObjectOutputStream s) throws IOException {
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
s.writeObject(e.key);
s.writeObject(e.value);
}
}
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
public LinkedHashMap(int initialCapacity) {
super(initialCapacity);
accessOrder = false;
}
public LinkedHashMap() {
super();
accessOrder = false;
}
public LinkedHashMap(Map<? extends K, ? extends V> m) {
super();
accessOrder = false;
putMapEntries(m, false);
}
//用以构造accessOrder==true的情况
public LinkedHashMap(int initialCapacity,
float loadFactor,
boolean accessOrder) {
super(initialCapacity, loadFactor);
this.accessOrder = accessOrder;
}
//遍历构造的队列
public boolean containsValue(Object value) {
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after) {
V v = e.value;
if (v == value || (value != null && value.equals(v)))
return true;
}
return false;
}
public V get(Object key) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return null;
if (accessOrder)
//structural modification
afterNodeAccess(e);
return e.value;
}
public V getOrDefault(Object key, V defaultValue) {
Node<K,V> e;
if ((e = getNode(hash(key), key)) == null)
return defaultValue;
if (accessOrder)
//structural modification
afterNodeAccess(e);
return e.value;
}
public void clear() {
super.clear();
head = tail = null;
}
/*
Returns true if this map should remove its eldest entry.
It provides the implementor with the opportunity to remove the eldest entry each time a new one is added.
This is useful if the map represents a LRU cache or other interesting implementations
*/
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
return false;
}
public Set<K> keySet() {
Set<K> ks = keySet;
if (ks == null) {
ks = new LinkedKeySet();
keySet = ks;
}
return ks;
}
//HashMap中这几个类都是final,所以继承不了了
final class LinkedKeySet extends AbstractSet<K> {
public final int size() { return size; }
public final void clear() { LinkedHashMap.this.clear(); }
public final Iterator<K> iterator() {
return new LinkedKeyIterator();
}
public final boolean contains(Object o) { return containsKey(o); }
public final boolean remove(Object key) {
return removeNode(hash(key), key, null, false, true) != null;
}
public final Spliterator<K> spliterator() {
return Spliterators.spliterator(this, Spliterator.SIZED |
Spliterator.ORDERED |
Spliterator.DISTINCT);
}
public final void forEach(Consumer<? super K> action) {
if (action == null)
throw new NullPointerException();
int mc = modCount;
//遍历队列
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
action.accept(e.key);
//保证此间代码同步
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
public Collection<V> values() {
Collection<V> vs = values;
if (vs == null) {
vs = new LinkedValues();
values = vs;
}
return vs;
}
final class LinkedValues extends AbstractCollection<V> {
public final int size() { return size; }
public final void clear() { LinkedHashMap.this.clear(); }
public final Iterator<V> iterator() {
return new LinkedValueIterator();
}
public final boolean contains(Object o) { return containsValue(o); }
public final Spliterator<V> spliterator() {
return Spliterators.spliterator(this, Spliterator.SIZED |
Spliterator.ORDERED);
}
public final void forEach(Consumer<? super V> action) {
if (action == null)
throw new NullPointerException();
int mc = modCount;
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
action.accept(e.value);
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
public Set<Map.Entry<K,V>> entrySet() {
Set<Map.Entry<K,V>> es;
return (es = entrySet) == null ? (entrySet = new LinkedEntrySet()) : es;
}
final class LinkedEntrySet extends AbstractSet<Map.Entry<K,V>> {
public final int size() { return size; }
public final void clear() { LinkedHashMap.this.clear(); }
public final Iterator<Map.Entry<K,V>> iterator() {
return new LinkedEntryIterator();
}
public final boolean contains(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Node<K,V> candidate = getNode(hash(key), key);
return candidate != null && candidate.equals(e);
}
public final boolean remove(Object o) {
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>) o;
Object key = e.getKey();
Object value = e.getValue();
return removeNode(hash(key), key, value, true, true) != null;
}
return false;
}
public final Spliterator<Map.Entry<K,V>> spliterator() {
return Spliterators.spliterator(this, Spliterator.SIZED |
Spliterator.ORDERED |
Spliterator.DISTINCT);
}
public final void forEach(Consumer<? super Map.Entry<K,V>> action) {
if (action == null)
throw new NullPointerException();
int mc = modCount;
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
action.accept(e);
if (modCount != mc)
throw new ConcurrentModificationException();
}
}
// Map overrides
public void forEach(BiConsumer<? super K, ? super V> action) {
if (action == null)
throw new NullPointerException();
int mc = modCount;
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
action.accept(e.key, e.value);
if (modCount != mc)
throw new ConcurrentModificationException();
}
public void replaceAll(BiFunction<? super K, ? super V, ? extends V> function) {
if (function == null)
throw new NullPointerException();
int mc = modCount;
for (LinkedHashMap.Entry<K,V> e = head; e != null; e = e.after)
e.value = function.apply(e.key, e.value);
if (modCount != mc)
throw new ConcurrentModificationException();
}
// Iterators
abstract class LinkedHashIterator {
LinkedHashMap.Entry<K,V> next;
LinkedHashMap.Entry<K,V> current;
int expectedModCount;
LinkedHashIterator() {
next = head;
expectedModCount = modCount;
current = null;
}
public final boolean hasNext() {
return next != null;
}
final LinkedHashMap.Entry<K,V> nextNode() {
LinkedHashMap.Entry<K,V> e = next;
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
if (e == null)
throw new NoSuchElementException();
current = e;
next = e.after;
return e;
}
public final void remove() {
Node<K,V> p = current;
if (p == null)
throw new IllegalStateException();
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
current = null;
K key = p.key;
removeNode(hash(key), key, null, false, false);
expectedModCount = modCount;
}
}
final class LinkedKeyIterator extends LinkedHashIterator
implements Iterator<K> {
public final K next() { return nextNode().getKey(); }
}
final class LinkedValueIterator extends LinkedHashIterator
implements Iterator<V> {
public final V next() { return nextNode().value; }
}
final class LinkedEntryIterator extends LinkedHashIterator
implements Iterator<Map.Entry<K,V>> {
public final Map.Entry<K,V> next() { return nextNode(); }
}
}其中:
+
-迭代时间与容量无关
LinkedHashMap的结构跟HashMap是一样的,也就是都baked by array。此处为什么“迭代时间与容量无关”,是因为LinkedHashMap内部维护了一个简单的链表队列【包含所有元素】,迭代的时候是对这个队列进行迭代,而不是像HashMap一样通过表迭代。
+怪不得读源码时觉得有些地方明明不重写HashMap也可以它却重写了。原来是因为这个性能问题啊
AbstractList帮我们实现了差不多所有方法,除了Tget(int) 、size() 、set(int, Object) 、add(int, E) 、remove(int) 。因而,接下来的两个实现中,重点关注这些就行。
-ArrayList
代码
--Implements all optional list operations, and permits all elements, including null.
-This class is roughly equivalent to Vector, except that it is unsynchronized.
-size(),isEmpty(),get(),set(),iterator(),listIterator()的时间复杂符是**常量级别(constant time),add()方法的时间复杂度是可变常量级别(amortized constant time),即为O(n)。大致上说,剩余方法的时间复杂度都是线性时间(linear time)。相较于LinkedList的实现,ArrayList的常量因子(constant factor)**较低。
-An application can increase the capacity of an ArrayList instance before adding a large number of elements using the ensureCapacity operation. This may reduce the amount of incremental reallocation.
-本身是线程不安全的,但可以通过封装类来实现线程同步。可以使用Collections.synchronizedList method.
--
List list = Collections.synchronizedList(new ArrayList(...));总体来说实现的很多方法跟想象中差别不大,有几个比较惊艳
-+
//1 cloneable
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//4
private static final long serialVersionUID = 8683452581122892189L;
//可以注意一下,初始=10.
private static final int DEFAULT_CAPACITY = 10;
//Shared empty array instance used for empty instances.
//7
private static final Object[] EMPTY_ELEMENTDATA = {};
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
//5、6
/*
The array buffer into which the elements of the ArrayList are stored.缓冲
ArrayList的容量=此数组的length
Any empty ArrayList with elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA will be expanded to DEFAULT_CAPACITY when the first element is added.
*/
transient Object[] elementData; // non-private to simplify nested class access
private int size;
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//in the order they are returned by c's iterator.
public ArrayList(Collection<? extends E> c) {
Object[] a = c.toArray();
if ((size = a.length) != 0) {
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, size, Object[].class);
}
//size==0
} else {
// replace with empty array.
elementData = EMPTY_ELEMENTDATA;
}
}
//把容量缩小为当前size。An application can use this operation to minimize the storage of an ArrayList instance.
public void trimToSize() {
//涉及List的结构性改变
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
//8
public void ensureCapacity(int minCapacity) {
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
//如果原size!=0,除非minCapacity=0,否则必须是要扩容一次的【函数要求】,
//因此设置为最小值0,以确保下面的if条件一定为true。
? 0
//如果为默认大小0,此处是必须扩为默认大小的
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
return minCapacity;
}
private void ensureCapacityInternal(int minCapacity) {
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private void ensureExplicitCapacity(int minCapacity) {
//修改list结构
//若minCapacity<elementData.length,本句modCount++始终执行。
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {
// overflow-conscious code
//capacity=length of elementData
int oldCapacity = elementData.length;
//每次扩1/2
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//防止溢出
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
public int size() {
return size;
}
public boolean isEmpty() {
return size == 0;
}
public boolean contains(Object o) {
return indexOf(o) >= 0;
}
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
//2
public Object clone() {
try {
//from Object.clone():
//the returned object should be obtained by calling super.clone.
ArrayList<?> v = (ArrayList<?>) super.clone();
//浅拷贝,只拷贝地址
v.elementData = Arrays.copyOf(elementData, size);
v.modCount = 0;
return v;
} catch (CloneNotSupportedException e) {
// this shouldn't happen, since we are Cloneable
throw new InternalError(e);
}
}
public Object[] toArray() {
return Arrays.copyOf(elementData, size);
}
public <T> T[] toArray(T[] a) {
if (a.length < size)
// Make a new array of a's runtime type, but my contents:
return (T[]) Arrays.copyOf(elementData, size, a.getClass());
System.arraycopy(elementData, 0, a, 0, size);
if (a.length > size)
a[size] = null;
return a;
}
// Positional Access Operations
E elementData(int index) {
return (E) elementData[index];
}
//这里我挺迷惑的,为什么还要再套一层elementData??
public E get(int index) {
rangeCheck(index);
return elementData(index);
}
public E set(int index, E element) {
rangeCheck(index);
E oldValue = elementData(index);
elementData[index] = element;
return oldValue;
}
public boolean add(E e) {
//既增加了modcount,也保证了capacity够用
ensureCapacityInternal(size + 1);
elementData[size++] = e;
return true;
}
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // Increments modCount!!
System.arraycopy(elementData, index, elementData, index + 1,
size - index);//src dest 移动数组
elementData[index] = element;
size++;
}
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work,自动清除无引用对象
return oldValue;
}
public boolean remove(Object o) {
if (o == null) {
for (int index = 0; index < size; index++)
if (elementData[index] == null) {
fastRemove(index);
return true;
}
} else {
for (int index = 0; index < size; index++)
if (o.equals(elementData[index])) {
fastRemove(index);
return true;
}
}
return false;
}
//这不是跟上面一模一样吗,为啥还要再写一遍?
private void fastRemove(int index) {
modCount++;
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index,
numMoved);
elementData[--size] = null; // clear to let GC do its work
}
public void clear() {
modCount++;
//9 我能不能直接猛一点:elementData=new Object[elementData.length]?
// clear to let GC do its work
for (int i = 0; i < size; i++)
elementData[i] = null;
size = 0;
}
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // Increments modCount
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
protected void removeRange(int fromIndex, int toIndex) {
modCount++;
int numMoved = size - toIndex;
System.arraycopy(elementData, toIndex, elementData, fromIndex,
numMoved);
// clear to let GC do its work
int newSize = size - (toIndex-fromIndex);
for (int i = newSize; i < size; i++) {
elementData[i] = null;
}
size = newSize;
}
private void rangeCheck(int index) {
if (index >= size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private void rangeCheckForAdd(int index) {
if (index > size || index < 0)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private String outOfBoundsMsg(int index) {
return "Index: "+index+", Size: "+size;
}
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, false);
}
public boolean retainAll(Collection<?> c) {
Objects.requireNonNull(c);
return batchRemove(c, true);
}
//这个complement做得很漂亮,兼顾实际意义又统一了代码
private boolean batchRemove(Collection<?> c, boolean complement) {
//局部变量
final Object[] elementData = this.elementData;
int r = 0, w = 0;
boolean modified = false;
try {
for (; r < size; r++)
if (c.contains(elementData[r]) == complement)
//原地平移,nice
//如果是removeall,此条件成立说明c不含有该元素,则保留该元素
//如果是retainall,此条件成立说明c含有该元素,则保留该元素
elementData[w++] = elementData[r];
} finally {
// Preserve behavioral compatibility with AbstractCollection,
// even if c.contains() throws.
if (r != size) {
System.arraycopy(elementData, r,
elementData, w,
size - r);
w += size - r;
}
if (w != size) {
// clear to let GC do its work
for (int i = w; i < size; i++)
elementData[i] = null;
modCount += size - w;
size = w;
modified = true;
}
}
return modified;
}
//序列化
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
/*from ObjectOutputStream:
Write the non-static and non-transient fields of the current class
to this stream.*/
s.defaultWriteObject();
//为啥要特地强调write size?
s.writeInt(size);
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
//保证一定基础的序列化同步
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
private void readObject(java.io.ObjectInputStream s) throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
//不大懂为什么这里的值被ignore了?
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
int capacity = calculateCapacity(elementData, size);
SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity);
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
public ListIterator<E> listIterator(int index) {
if (index < 0 || index > size)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
public ListIterator<E> listIterator() {
return new ListItr(0);
}
public Iterator<E> iterator() {
return new Itr();
}
/**
* AbstractList.Itr的优化版本
*/
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
int expectedModCount = modCount;
Itr() {}
public boolean hasNext() {
return cursor != size;
}
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
//内部类访问外部类this的方法
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
//change here
//确实AbstractList的那个remove应该更适用于LinkedList
ArrayList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
//这里不抛异常吗
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
//好像确实是并发修改了,毕竟上面已经test过i<size<capacity了
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
//10
cursor = i;
lastRet = i - 1;
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
/**
* AbstractList.ListItr的优化版本
*/
private class ListItr extends Itr implements ListIterator<E> {
ListItr(int index) {
super();
cursor = index;
}
public boolean hasPrevious() {
return cursor != 0;
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
//11 防止并发修改,比如在这期间进行了trim
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, 0, fromIndex, toIndex);
}
static void subListRangeCheck(int fromIndex, int toIndex, int size) {
if (fromIndex < 0)
throw new IndexOutOfBoundsException("fromIndex = " + fromIndex);
if (toIndex > size)
throw new IndexOutOfBoundsException("toIndex = " + toIndex);
if (fromIndex > toIndex)
throw new IllegalArgumentException("fromIndex(" + fromIndex +
") > toIndex(" + toIndex + ")");
}
//依然内部类。不过这里应该用了个作用域的性质。相比于AbstractList定义在List类外部的
//Sublist,应该会更优先使用定义在内部的Sublist
//12 注意,这里的sublist没有直接extends ArrayList
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
//新增成员
private final int parentOffset;
private final int offset;
int size;
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
//这个parentOffset应该是指相较于最近的父亲的偏移量,offset应该就是相较于最底层的父亲的偏移量
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
public E set(int index, E e) {
rangeCheck(index);
checkForComodification();
E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
}
public E get(int index) {
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index);
}
public int size() {
checkForComodification();
return this.size;
}
public void add(int index, E e) {
rangeCheckForAdd(index);
checkForComodification();
//13
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
this.size++;
}
public E remove(int index) {
rangeCheck(index);
checkForComodification();
E result = parent.remove(parentOffset + index);
this.modCount = parent.modCount;
this.size--;
return result;
}
protected void removeRange(int fromIndex, int toIndex) {
checkForComodification();
parent.removeRange(parentOffset + fromIndex,
parentOffset + toIndex);
this.modCount = parent.modCount;
this.size -= toIndex - fromIndex;
}
public boolean addAll(Collection<? extends E> c) {
return addAll(this.size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
int cSize = c.size();
if (cSize==0)
return false;
checkForComodification();
parent.addAll(parentOffset + index, c);
this.modCount = parent.modCount;
this.size += cSize;
return true;
}
public Iterator<E> iterator() {
return listIterator();
}
public ListIterator<E> listIterator(final int index) {
checkForComodification();
rangeCheckForAdd(index);
final int offset = this.offset;
return new ListIterator<E>() {
int cursor = index;
int lastRet = -1;
int expectedModCount = ArrayList.this.modCount;
public boolean hasNext() {
//内部内部类还可以访问外部类和外部外部类
return cursor != SubList.this.size;
}
public E next() {
checkForComodification();
int i = cursor;
if (i >= SubList.this.size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (offset + i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[offset + (lastRet = i)];
}
public boolean hasPrevious() {
return cursor != 0;
}
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (offset + i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[offset + (lastRet = i)];
}
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = SubList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (offset + i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[offset + (i++)]);
}
// update once at end of iteration to reduce heap write traffic
lastRet = cursor = i;
checkForComodification();
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
SubList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = ArrayList.this.modCount;
} catch (IndexOutOfBoundsException ex) {
//确实要是IndexOutOfBounds的话,就说明lastRet改了,说明Concurrent了
throw new ConcurrentModificationException();
}
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.set(offset + lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
SubList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = ArrayList.this.modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (expectedModCount != ArrayList.this.modCount)
throw new ConcurrentModificationException();
}
};
}
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, offset, fromIndex, toIndex);
}
public Spliterator<E> spliterator() {
checkForComodification();
return new ArrayListSpliterator<E>(ArrayList.this, offset,
offset + this.size, this.modCount);
}
}
public void forEach(Consumer<? super E> action) {
Objects.requireNonNull(action);
final int expectedModCount = modCount;
final E[] elementData = (E[]) this.elementData;
final int size = this.size;
//一修改就会寄
for (int i=0; modCount == expectedModCount && i < size; i++) {
action.accept(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
public Spliterator<E> spliterator() {
return new ArrayListSpliterator<>(this, 0, -1, 0);
}
/** Index-based split-by-two, lazily initialized Spliterator */
//基于索引的二分法,懒加载的 Spliterator
static final class ArrayListSpliterator<E> implements Spliterator<E> {...}
public boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
int removeCount = 0;
//666,用了类似掩码的思想,这样就能避免多次移动数组了,实现O(n),很不错
final BitSet removeSet = new BitSet(size);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
final E element = (E) elementData[i];
if (filter.test(element)) {
//set:Sets the bit at the specified index to true.
removeSet.set(i);
removeCount++;
}
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
// shift surviving elements left over the spaces left by removed elements
final boolean anyToRemove = removeCount > 0;
if (anyToRemove) {
final int newSize = size - removeCount;
for (int i=0, j=0; (i < size) && (j < newSize); i++, j++) {
//nextClearBit:Returns the index of the first bit that is set to false
//false表示不移走,true表示移走
i = removeSet.nextClearBit(i);
elementData[j] = elementData[i];
}
for (int k=newSize; k < size; k++) {
elementData[k] = null; // Let gc do its work
}
this.size = newSize;
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
return anyToRemove;
}
public void replaceAll(UnaryOperator<E> operator) {
Objects.requireNonNull(operator);
final int expectedModCount = modCount;
final int size = this.size;
for (int i=0; modCount == expectedModCount && i < size; i++) {
elementData[i] = operator.apply((E) elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
public void sort(Comparator<? super E> c) {
final int expectedModCount = modCount;
Arrays.sort((E[]) elementData, 0, size, c);
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
modCount++;
}
}SortedMap(I)
+A Map that further provides a total ordering on its keys.
+The map is ordered according to the natural ordering of its keys, or by a Comparator typically provided at sorted map creation time.
+All keys inserted into a sorted map must implement the Comparable interface (or be accepted by the specified comparator).
+-
其中:
-
- -
Cloneable接口
与RandomAccess一样,都是一个规定性质的接口。
---A class implements the Cloneable interface to indicate to the Object.clone() method that it is legal for that method to make a field-for-field copy of instances of that class.
-Classes that implement this interface should override Object.clone (which is protected) with a public method.
+关于这部分,详细见sorted set
- -
clone与浅拷贝(shallow copy)
- -
public static void main(String[] args) {
ArrayList<Student> arr = new ArrayList<>();
arr.add(new Student("lylt",15));
ArrayList<Student> cl = (ArrayList)arr.clone();
System.out.println(cl.get(0).toString());
cl.get(0).name="Sam";
System.out.println(cl.get(0).toString());
System.out.println(arr.get(0).toString());
cl.set(0,new Student("HWX",19));
System.out.println(cl.get(0).toString());
System.out.println(arr.get(0).toString());
}
/*运行结果:
name :lyltage :15
name :Samage :15
name :Samage :15
name :HWXage :19
name :Samage :15
*/结合内部代码可知,确实跟上面那个toArray的原理是一样的,只拷贝地址。
-clone是浅拷贝。
- -![]()
- -
关于子类继承到的父类内部类
本来在犹豫,子类默认继承到的内部类里面用到的外部类方法的版本是取父还是取子,经过以下实验可知,是取能访问到的最新版本。
-+
public class Main {
public static void main(String[] args) {
ChildOuter chldot = new ChildOuter();
chldot.printname();
chldot.in.printall();
}
}
/*结果:子类声明为private的成员字段不能被从父类继承而来的方法访问到
Father
I am father!*/
class FatherOuter{
String name="Father";
private void print(){
System.out.println("I am father!");
}
public Inner in = new Inner();
public class Inner{
public int haha=1;
void printall(){
print();
}
}
public void printname(){
System.out.println(name);
}
}
class ChildOuter extends FatherOuter{
private String name="Child";
private void print(){System.out.println("I am child!");}
}最大的特点就是可以人为定义有序并且有sub map
+代码:
-
public interface SortedMap<K,V> extends Map<K,V> {
Comparator<? super K> comparator();
SortedMap<K,V> subMap(K fromKey, K toKey);
SortedMap<K,V> headMap(K toKey);
SortedMap<K,V> tailMap(K fromKey);
//也是默认第一个是低的最后一个是高的,就跟LHM的第一个是最少使用,最后一个是最近使用一样
//Returns the first (lowest) key currently in this map.
K firstKey();
//Returns the last (highest) key currently in this map.
K lastKey();
Set<K> keySet();
Collection<V> values();
Set<Map.Entry<K, V>> entrySet();
}如若把Father和Child内的print类都换成public:
-+
class FatherOuter{
//...
public void print(){
System.out.println("I am father!");
}
//...
}
class ChildOuter extends FatherOuter{
//...
public void print(){System.out.println("I am child!");}
}
/*结果:访问最新版本
I am child!*/NavigableMap
++A SortedMap extended with navigation methods returning the closest matches for given search targets.
+The performance of ascending operations and views is likely to be faster than that of descending ones.
+submap都多加了几个参数:inclusive or exclusive
+其entry不支持setValue,只能通过map自身的put方法改变value。因为要求前者只是map的快照
+跟navigable set差不多的定义
+代码:
-
public interface NavigableMap<K,V> extends SortedMap<K,V> {
Map.Entry<K,V> lowerEntry(K key);
K lowerKey(K key);
Map.Entry<K,V> floorEntry(K key);
K floorKey(K key);
Map.Entry<K,V> ceilingEntry(K key);
K ceilingKey(K key);
Map.Entry<K,V> higherEntry(K key);
K higherKey(K key);
Map.Entry<K,V> firstEntry();
Map.Entry<K,V> lastEntry();
Map.Entry<K,V> pollFirstEntry();
Map.Entry<K,V> pollLastEntry();
NavigableMap<K,V> descendingMap();
NavigableSet<K> navigableKeySet();
NavigableSet<K> descendingKeySet();
NavigableMap<K,V> subMap(K fromKey, boolean fromInclusive,
K toKey, boolean toInclusive);
NavigableMap<K,V> headMap(K toKey, boolean inclusive);
NavigableMap<K,V> tailMap(K fromKey, boolean inclusive);
SortedMap<K,V> subMap(K fromKey, K toKey);
SortedMap<K,V> headMap(K toKey);
SortedMap<K,V> tailMap(K fromKey);
}
//结论:内部类可以访问外部类所有不论私有还是公有的资源;会优先访问最新版本【父子类而言】
//子类声明为private的成员字段不能被从父类继承而来的方法访问到,只会访问能访问到的最新版本- -
序列化versionID
ArrayList实现了java.io.Serializable接口,故而可以被序列化和反序列化,就需要有个序列化版本ID
-What is a serialVersionUID and why should I use it?
--这东西是用来在反序列化的时候保证收到的对象和发送的对象的类是相同的。If the receiver has loaded a class for the object that has a different
-serialVersionUID
than that of the corresponding sender’s class, then deserialization will result in anInvalidClassException
.The field serialVersionUID must be static, final, and of type
-long
.If a serializable class does not explicitly declare a
+serialVersionUID
, then the serialization runtime will calculate a defaultserialVersionUID
value for that class based on various aspects of the class, as described in the Java(TM) Object Serialization Specification.TreeMap
+-NavigableMap的红黑树实现
+key不允许空,空会抛出异常
+Note that this implementation is not synchronized.
+fail-fast
+All Map.Entry pairs returned by methods in this class and its views represent snapshots of mappings at the time they were produced. They do not support the Entry.setValue method. (Note however that it is possible to change mappings in the associated map using put.)【navigable map的性质】
transient
--transient是短暂的意思。对于transient 修饰的成员变量,在类的实例对象的序列化处理过程中会被忽略。 因此,transient变量不会贯穿对象的序列化和反序列化,生命周期仅存于调用者的内存中而不会写到磁盘里进行持久化。
-在持久化对象时,对于一些特殊的数据成员(如用户的密码,银行卡号等),我们不想用序列化机制来保存它。为了在一个特定对象的一个成员变量上关闭序列化,可以在这个成员变量前加上关键字transient。
-注意static修饰的静态变量天然就是不可序列化的。一个静态变量不管是否被transient修饰,均不能被序列化(如果反序列化后类中static变量还有值,则值为当前JVM中对应static变量的值)。序列化保存的是对象状态,静态变量保存的是类状态,因此序列化并不保存静态变量。
-使用场景
+
(1)类中的字段值可以根据其它字段推导出来,如一个长方形类有三个属性长度、宽度、面积,面积不需要序列化。
(2) 一些安全性的信息,一般情况下是不能离开JVM的。
(3)如果类中使用了Logger实例,那么Logger实例也是不需要序列化的具体代码就不看了
+对Collection和Map的总结
+
+]]>- -
fail-fast
+-The iterators returned by all of this class’s “collection view methods” are fail-fast: if the map is structurally modified at any time after the iterator is created, in any way except through the iterator’s own remove method, the iterator will throw a ConcurrentModificationException. Thus, in the face of concurrent modification, the iterator fails quickly and cleanly, rather than risking arbitrary, non-deterministic behavior at an undetermined time in the future.
+Note that the fail-fast behavior of an iterator cannot be guaranteed as it is, generally speaking, impossible to make any hard guarantees in the presence of unsynchronized concurrent modification. Fail-fast iterators throw ConcurrentModificationException on a best-effort basis. Therefore, it would be wrong to write a program that depended on this exception for its correctness: the fail-fast behavior of iterators should be used only to detect bugs.
但其实还有一个问题,它这边源码对这个transient的注释是:
-non-private to simplify nested class access
,“非私有以简化嵌套类访问”。问题是这个transient和类的公有还是私有有什么关系呢?Why is the data array in java.util.ArrayList package-private?
-其实没怎么看懂23333
+都使用了modcount进行并发检查,都具有fail-fast的特点(关于此的详细解说,可见AbstractList第四点和List第二点),因而只允许在迭代中使用迭代器的remove方法进行结构性改变。【注意:对于LinkedHashMap中access order排序,get方法也是structural modification,因而也只能通过迭代器的next方法获取元素】
ArrayList的elementData虽然被transient修饰,但仍然能够序列化
+- +
not synchronized
上面介绍到的几个类,除了Vector外,都是线程不同步的。可以用此方式让其线程同步。
+
Map m = Collections.synchronizedMap(new LinkedHashMap(...));- -
是否允许null
除了TreeSet、TreeMap、ArrayDeque之外,都是允许空(key/value)的
关于static的空数组
EMPTY_ELEMENTDATA和DEFAULTCAPACITY_EMPTY_ELEMENTDATA这两个空数组可用于表示两种情况:
-new ArrayList(0) ->EMPTY_ELEMENTDATA
-new ArrayList() ->DEFAULTCAPACITY_EMPTY_ELEMENTDATA
-之所以用静态,是提取了共性:不论是需要什么ArrayList,其空形态不都一样吗(
-这样可以避免了制造对象的浪费。very good。
+- -
是否有序
List都是插入序,HashSet无需,HashMap也无序(但其实算是有内部桶序的),LinkedHashMap有插入序和LRU序(依靠内部增加简单队列的消耗),TreeSet有序,TreeMap有序【这俩靠红黑树的遍历顺序(二叉搜索树嘛)】。
- -
关于扩容的连环计
我其实觉得不必写那么麻烦……
- -经过测试替换可得,确实可以像我那样写。
-
//此处的ArrayList是魔改过的
//now the capacity is 0
ArrayList a = new ArrayList<>();
System.out.println(a.getCapacity());
//扩容到DEFAULT
a.add(new Student("Lily",15));
System.out.println(a.getCapacity());
addStudent(a);
System.out.println(a.getCapacity());
/*运行结果:
0
10
33
*/关于clear我的写法
-+
ArrayList a = new ArrayList<>();
addStudent(a);
long sum1=0;
for (int i=0;i<50000;i++){
long startTime = System.currentTimeMillis();
a.clear1();
addStudent(a);
long endTime = System.currentTimeMillis();
sum1+=endTime-startTime;
}
//sum1/=500;
System.out.println("clear1: "+sum1);
sum1=0;
for (int i=0;i<50000;i++){
long startTime = System.currentTimeMillis();
a.clear2();
addStudent(a);
long endTime = System.currentTimeMillis();
sum1+=endTime-startTime;
}
//sum1/=500;
System.out.println("clear2: "+sum1);- +
实现的约定接口
都Cloneable,Serializable
+ArrayList/Vector:RandomAccess
+
此为《程序员的自我修养:链接、装载与库》(俞甲子,石凡,潘爱民)的看书总结。
+ +链接前的编译阶段可以生成.o文件,.o文件是ELF文件,里面含有段表、符号表、bss段、common段等链接辅助段。
+执行可执行文件时,首先要通过fork创建一个新的子进程,然后要通过exec为子进程制定可执行文件装载逻辑。在exec系统调用中,会进行elf文件的读取解析。它会解析elf header,根据其各种信息将程序copy到内存(进程的虚拟地址空间)中,后者也就是我们所研究的装载。
+装载不同于“节(section),是以”段“(segment)为单位。进程虚拟地址空间被分为很多个VMA,每个VMA都有不同的属性(如权限,可读可写可执行)。ELF可执行文件除去在链接/编译过程中被视为一个个连续的节之外,它还会在链接的时候根据每个节的属性不同重排节,把属性相同的节连续放在一起成为一个个段。链接的时候还会形成程序头表。对应关系:节——段表,段——程序头表
+装载进内存时,是以段为单位,一个段就对应着一个VMA。
+内核通过execve
系统调用装载完ELF可执行文件以后就返回到用户空间,将控制权交给程序的入口。
对于不同链接形式的ELF可执行文件,这个程序的入口是有区别的。对于静态链接的可执行文件来说,程序的入口就是ELF文件头里面的e_entry指定的入口地址;对于动态链接的可执行文件来说,如果这时候把控制权交给e_entry指定的入口地址,那么肯定是不行的,因为可执行文件所依赖的共享库还没有被装载,也没有进行动态链接。所以对于动态链接的可执行文件,内核会分析它的动态链接器地址(在“.interp”段),将动态链接器映射至进程地址空间,然后把控制权交给动态链接器。
+编译器编译源代码后生成的文件叫做目标文件(Object文件),目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本身就是按照可执行文件格式存储的,只是跟真正的可执行文件在结构上稍有不同。
+Linux的可执行文件遵从ELF的结构模式。
+ELF可以分为这几类:
+也即.o(可重定位文件)、.exe(无后缀)(可执行文件)、.so(动态链接库)、.a(静态链接库)、core dump。
+目标文件中的内容至少有编译后的机器指令代码、数据。没错,除了这些内容以外,目标文件中还包括了链接时所须要的一些信息,比如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以“节”(Section)的形式存储,有时候也叫“段”(Segment)。
+众所周知,强大而又复杂的C++拥有类、继承、虚机制、重载、名称空间等这些特性,它们使得符号管理更为复杂。最简单的例子,两个相同名字的函数func(int)和func(double),尽管函数名相同,但是参数列表不同,这是C++里面函数重载的最简单的一种情况,那么编译器和链接器在链接过程中如何区分这两个函数呢?为了支持C++这些复杂的特性,⼈们发明了符号修饰(Name Decoration)或符号改编(Name Mangling)的机制。
+C++为了与C兼容,在符号的管理上,C++有一个用来声明或定义一个C的符号的“extern “C””关键字用法:
+extern ”C” { |
经测试发现不分伯仲()也确实差距应该很小2333
-不过依照一个回答:
+C++编译器会将在extern “C” 的大括号内部的代码当作C语言代码处理。所以很明显,上面的代码中,C++的名称修饰机制将不会起作用。
+多个目标文件中含有相同名字全局符号的定义,那么这些目标文件链接的时候将会出现符号重复定义的错误。这种符号的定义可以被称为强符号(Strong Symbol)。有些符号的定义可以被称为弱符号(Weak Symbol)。对于C/C++语言来说,编译器默认函数和初始化了的全局变量为强符号,未初始化的全局变量为弱符号。我们也可以通过GCC的__attribute__((weak))
来定义任何一个强符号为弱符号。
目前我们所看到的对外部目标文件的符号引用在目标文件被最终链接成可执行文件时,它们须要被正确决议,如果没有找到该符号的定义,链接器就会报符号未定义错误,这种被称为强引用(Strong Reference)。与之相对应还有一种弱引用(Weak Reference),在处理弱引用时,如果该符号有定义,则链接器将该符号的引用决议;如果该符号未被定义,则链接器对于该引用不报错。
+链接器处理强引用和弱引用的过程几乎一样,只是对于未定义的弱引用,链接器不认为它是一个错误。一般对于未定义的弱引用,链接器默认其为0,或者是一个特殊的值,以便于程序代码能够识别。
+__attribute__ ((weakref)) void foo(); |
这种弱符号和弱引用对于库来说十分有用,比如库中定义的弱符号可以被用户定义的强符号所覆盖,从而使得程序可以使用自定义版本的库函数。
--Your approach explicitly throws the backing array away. The existing implementation attempts to reuse it. So even if your approach is faster in isolation, in practice it will almost certainly be less performant. Since calling
+clear()
is a sign you intend to reuse theArrayList
.这里很帅,有条件编译那味了
其实感觉我的clear说不定花销更大,毕竟要创建一个新对象(
- -What is heap write traffic and why it is required in ArrayList?
-详见第二个答案。
-栈放在一级缓存,堆放在二级缓存
-总之意思就是成员变量写在堆里,局部变量写在栈里,做
-while (i != size && modCount == expectedModCount) { |
在Linux程序的设计中,如果一个程序被设计成可以支持单线程或多线程的模式,就可以通过弱引用的方法来判断当前的程序是链接到了单线程的Glibc库还是多线程的Glibc库(是否在编译时有-lpthread选项),从而执行单线程版本的程序或多线程版本的程序。我们可以在程序中定义一个pthread_create函数的弱引用,然后程序在运行时动态判断是否链接到pthread库从而决定执行多线程版本还是单线程版本:
+
|
比做
-while (cursor != size && modCount == expectedModCount) { |
++弱符号大概是说该变量可以被定义多次,最终链接时再进行决议;弱引用大概是说该变量(函数)可以不被定义。
+
静态链接库(.a文件)本质上是一堆.o文件的集合。静态链接的基本过程:
+test.c ——(compile)——>test.o——(link)——>test(ELF exe) |
花销更小
+静态链接其实也就是分为两大步骤:
+空间与地址分配
+将input file的各个段都连在一起,并且为符号分配虚拟地址
is i >= elementData.length in ArrayList::iterator redundant?
-public E previous() { |
如果 user invoke trimToSize method ,就会导致在checkForComodification();
和if (i >= elementData.length)
之间发生ArrayIndexOutOfBounds
。而在if (i >= elementData.length)
之后trim没有影响,因为我们的局部变量已经保存了原来的elementData,此时再trim只是修改成员变量的elementData,局部变量依然不变。
符号解析与重定位
+扫描所有输入文件的符号表形成全局符号表;
private class SubList extends AbstractList<E> implements RandomAccess |
sublist没有extends Cloneable, java.io.Serializable这两个接口
+重定向
+可执行文件基本可以确定自己在进程虚拟空间中的起始位置,因为可执行文件往往是第一个被加载的文件,它可以选择一个固定空闲的地址。因而,在link中,可执行文件的地址都以确定,就可以开始进行重定向。
+通过重定向表对所有UNDEF的符号进行地址修正,包括相对地址修正和绝对地址修正。
首先,这两个是同一个吗?其次,这俩是否是同一个跟sub的级数有关系吗,就比如一级sub都是同一个,多级sub就不是同一个了?
-经过对ArrayList的一些public和以下代码的测试,得出结论:这两个只有在第一级sub的时候是同一个。parent指向直系父亲,ArrayList.this指向root父亲。
-//In ArrayList: |
总之,ArrayList的sublist实现方式相当于串成了一条父子继承串,多级sub,至于这么干相比原来的只有两级父子关系的方法好在哪就不知道了
+-提供顺序访问list的基本骨架
-To implement a list the programmer needs only to extend this class and provide implementations for the listIterator and size methods. For an unmodifiable list, the programmer need only implement the list iterator’s hasNext, next, hasPrevious, previous and index methods.
+
For a modifiable list the programmer should additionally implement the list iterator’s set method. For a variable-size list the programmer should additionally implement the list iterator’s remove and add methods.在link中,会读取
+test.o
以及lib.a
中的符号表,完成重定向(绝对地址和相对地址)以及节的重排组织,最终组合形成以段为单位的可执行文件test
。可执行文件
+test
会通过系统调用exevec
被装载进物理内存(lazy allocation),分段映射到进程的虚拟地址空间。静态链接的缺陷是,由于重定向在link过程完成,故而同一份共享库在物理内存中会有多份copy,极大占用物理内存和磁盘空间。优点是速度快。
+动态链接
(下文注意区分两个概念:可执行文件和动态链接库)
+动态链接库(.so)不同于静态链接库。
++ +
test.c ——(compile)——>test.o——(link)——>test(ELF exe)
↑
lib.so在link中,仅会读入动态链接库的符号表,对于动态链接库的符号仅会将其标记为动态符号,而不会对其进行重定向。
+可执行文件
+test
会通过系统调用exevec
被装载进物理内存(lazy allocation),分段映射到进程的虚拟地址空间。静态链接是per-process一份库,内存中有多份库;动态链接是per-process一份库,内存也只有一份库。并且虚拟地址动态分配,也即库映射到进程地址空间的哪块VMA是不确定的。
+由于动态链接库被装载时的虚拟地址不确定,所以对于动态链接库和可执行文件代码中与动态链接相关的绝对地址,不能简单采用装载时重定向的方法来对其重定向,否则会破坏其共享性和不变性。
++-试想一下,每个进程加载的动态链接库的地址都不同,那岂不是每个进程的动态链接库的重定向结果都不一样,指令都不一样,不就寄了。
代码:
+
public abstract class AbstractSequentialList<E> extends AbstractList<E> {
protected AbstractSequentialList() {
}
public E get(int index) {
try {
//1
return listIterator(index).next();
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public E set(int index, E element) {
try {
ListIterator<E> e = listIterator(index);
E oldVal = e.next();
e.set(element);
return oldVal;
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public void add(int index, E element) {
try {
listIterator(index).add(element);
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
public E remove(int index) {
try {
ListIterator<E> e = listIterator(index);
E outCast = e.next();
e.remove();
return outCast;
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
// Bulk Operations
public boolean addAll(int index, Collection<? extends E> c) {
try {
boolean modified = false;
ListIterator<E> e1 = listIterator(index);
Iterator<? extends E> e2 = c.iterator();
while (e2.hasNext()) {
e1.add(e2.next());
modified = true;
}
return modified;
} catch (NoSuchElementException exc) {
throw new IndexOutOfBoundsException("Index: "+index);
}
}
// Iterators
public Iterator<E> iterator() {
return listIterator();
}
public abstract ListIterator<E> listIterator(int index);
}所以我们此时进行了一个牛逼到家、惊天动地、无人能比的操作。
+我们可以分离.text和.data,前者作为“共享”语义保持不变性,后者则在每个进程地址空间中都留存一个copy。然后,我们将所有立即数绝对寻址的地方,换为间接寻址!也即,把那个立即数绝对地址改成一个变量,变量值在.data段中存储。这样一来,就成功把绝对寻址替换成了相对寻址。加载的时候也只需将虚拟地址填进可变的.data就行。不得不说真是十分地巧妙。
+这个从相对地址——绝对地址的转换过程,由ELF中的一个新段GOT表(.got)来实现。在link时加入了动态链接库符号表的可执行文件,以及动态链接库本身,都使用了.got段,以PIC形式出现。
+这个操作就是所谓的“地址无关代码”,通过
+-fPIC
选项,就可以将代码编译为一个地址无关的程序。使用PIC模式编译的共享对象,对于模块内部的函数调用也是采用跟模块外部函数调用一样的方式,即使用GOT/PLT的方式。+小trick:如何区分一个DSO是否为PIC
+-
readelf -d foo.so | grep TEXTREL其中:
-
-- -
get和set方法通过Iterator实现
随机访问的AbstractList的iterator的方法借助了主类的get和set,跟这里正好反过来。但注意哈,下面的LinkedList实现把以上差不多所有的方法都重写了,因而get和set之类的方法,LinkedList并不是依靠迭代器的。
-LinkedList
-+双向链表,实现List和Deque
-并发不安全。List list = Collections.synchronizedList(new LinkedList(…));
-印象:漂亮的指针操作,以及好像很少抛出异常,还有很多很多繁琐的方法(
+如果上面的命令有任何输出,那么foo.so就不是PIC的,否则就是PIC的。PIC的DSO是不会包含任何代码段重定位表的,TEXTREL表⽰代码段重定位表地址。
+这也很好理解,因为PIC本质上就是把代码段重定位转化为了数据段重定位。
+除了动态链接库中的寻址(对变量和函数)需要使用PIC之外,对可执行文件的全局变量也需要使用特殊的机制。ELF共享库中的全局变量都类似以弱引用形式存在。当全局变量在主程序extern时,若该变量在共享库中初始化了,那么加载之后要把共享库的数据copy进主程序;否则,该变量值都以主模块为准。
+++这段原因解释看书真没懂,详情340页开始。
+不过感觉它可能说的有点问题,我个人认为全局变量需要使用这种以方式存在,是为了保证进程资源独立。如果变量都以共享库中的数据值为准,那各个进程共享共享库不就乱了。。。你改一下我改一下
+因而,总的装载流程是:
+未优化情况下,在可执行文件被装载之前,先将其依赖的所有动态链接库加载进内存。若其所需的动态链接库已经被映射到物理内存,则将其装载到进程虚拟地址空间;否则,则映射到物理内存,并且装载到进程虚拟地址空间。然后,在装载动态链接库后,扫描可执行文件.got段符号进行装载时重定向(依据已经装载了的动态链接库虚拟地址来计算符号地址)即可。
+但可以注意到这一步还是有优化空间。所以我们采取延迟绑定(PLT)的方法,第一次访问到动态链接库符号时,才对其进行重定向并填入.got中。
+动态链接的缺点就是太慢了,一是因为PIC导致模块内部函数和全局变量也需要以.got形式访问,加了层寻址;二是运行时重定向开销巨大。对于前者,模块内部函数可以使用static关键字修饰;对于后者,采用PLT。
++
显式运行时链接
支持动态链接的系统往往都支持一种更加灵活的模块加载方式,叫做显式运行时链接(Explicit Runtime Linking),有时候也叫做运行时加载。也就是让程序自己在运行时控制加载指定的模块,并且可以在不需要该模块时将其卸载。
+++也就是说,前面介绍的动态链接库是由动态链接器自动完成的,程序啥也不知道;这里的动态装载库是程序自己控制的,所以会提供给程序各种API。
+而动态库的装载则是通过一系列由动态链接器提供的API,具体地讲共有4个函数:打开动态库(
+dlopen
)、查找符号(dlsym
)、错误处理(dlerror
)以及关闭动态库(dlclose
),程序可以通过这几个API对动态库进行操作。这几个API的实现是在/lib/libdl.so.2
里面,它们的声明和相关常量被定义在系统标准头文件<dlfcn.h>
。+-很有意思的是,如果我们将filename这个参数设置为0,那么dlopen返回的将是全局符号表的句柄,也就是说我们可以在运行时找到全局符号表里面的任何一个符号,并且可以执行它们,这有些类似高级语言反射(Reflection)的特性。全局符号表包括了程序的可执行文件本身、被动态链接器加载到进程中的所有共享模块以及在运行时通过dlopen打开并且使用了RTLD_GLOBAL方式的模块中的符号。
代码:
+
public class LinkedList<E>
extends AbstractSequentialList<E>
implements List<E>, Deque<E>, Cloneable, java.io.Serializable
{
//怎么连size也是transient?这不是代表着该对象的信息吗(
transient int size = 0;
transient Node<E> first;
transient Node<E> last;
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
//调用空构造器
this();
addAll(c);
}
//把e接在链表头
private void linkFirst(E e) {
final Node<E> f = first;
final Node<E> newNode = new Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
// Inserts element e before non-null Node succ.
void linkBefore(E e, Node<E> succ) {
// assert succ != null;
final Node<E> pred = succ.prev;
final Node<E> newNode = new Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}
private E unlinkFirst(Node<E> f) {
// assert f == first && f != null;
//这里就只靠注释会不会危险了()不过也确实会自动帮我们抛出NullPointerException的
//而且我在想,不是first已经指向头结点了吗,那你为什么还要把头结点作为参数传进来。。。
final E element = f.item;
final Node<E> next = f.next;
f.item = null;
f.next = null; // help GC
first = next;
if (next == null)
last = null;
else
next.prev = null;
size--;
modCount++;
return element;
}
//3
private E unlinkLast(Node<E> l) {
// assert l == last && l != null;
final E element = l.item;
final Node<E> prev = l.prev;
l.item = null;
l.prev = null; // help GC
last = prev;
if (prev == null)
first = null;
else
prev.next = null;
size--;
modCount++;
return element;
}
E unlink(Node<E> x) {
// assert x != null;
final E element = x.item;
final Node<E> next = x.next;
final Node<E> prev = x.prev;
if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}
if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}
x.item = null;
size--;
modCount++;
return element;
}
public E getFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}
public E getLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}
public E removeFirst() {
final Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return unlinkFirst(f);
}
public E removeLast() {
final Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return unlinkLast(l);
}
public void addFirst(E e) {linkFirst(e);}
public void addLast(E e) {linkLast(e);}
public boolean contains(Object o) {return indexOf(o) != -1;}
public int size() {return size;}
public boolean add(E e) {
linkLast(e);
return true;
}
public boolean remove(Object o) {
if (o == null) {
for (Node<E> x = first; x != null; x = x.next) {
if (x.item == null) {
unlink(x);
return true;
}
}
} else {
for (Node<E> x = first; x != null; x = x.next) {
if (o.equals(x.item)) {
unlink(x);
return true;
}
}
}
return false;
}
public boolean addAll(Collection<? extends E> c) {
return addAll(size, c);
}
public boolean addAll(int index, Collection<? extends E> c) {
checkPositionIndex(index);
Object[] a = c.toArray();
int numNew = a.length;
if (numNew == 0)
return false;
//分成两段
Node<E> pred, succ;
if (index == size) {
succ = null;
pred = last;
} else {
succ = node(index);
pred = succ.prev;
}
//往中间加料
for (Object o : a) {
E e = (E) o;
Node<E> newNode = new Node<>(pred, e, null);
if (pred == null)
first = newNode;
else
pred.next = newNode;
pred = newNode;
}
//合起来
if (succ == null) {
last = pred;
} else {
pred.next = succ;
succ.prev = pred;
}
size += numNew;
modCount++;
return true;
}
public void clear() {
// 1
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator
for (Node<E> x = first; x != null; ) {
Node<E> next = x.next;
x.item = null;
x.next = null;
x.prev = null;
x = next;
}
first = last = null;
size = 0;
modCount++;
}
// Positional Access Operations
public E get(int index) {
checkElementIndex(index);
return node(index).item;
}
public E set(int index, E element) {
checkElementIndex(index);
Node<E> x = node(index);
E oldVal = x.item;
x.item = element;
return oldVal;
}
public void add(int index, E element) {
checkPositionIndex(index);
if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}
public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}
Node<E> node(int index) {
// assert isElementIndex(index);
//所以为啥不能check一下?
//做了个小小的优化,如果在前半就从开头找,在后半就从最后往前找
if (index < (size >> 1)) {
Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}
// Search Operations
public int indexOf(Object o) {...}
public int lastIndexOf(Object o) {...}
// Queue operations.
//部分省略
//always return true
public boolean offer(E e) {
return add(e);
}
// Deque operations
//省略
public boolean removeFirstOccurrence(Object o) {
//666
return remove(o);
}
public boolean removeLastOccurrence(Object o) {...}
public ListIterator<E> listIterator(int index) {
checkPositionIndex(index);
return new ListItr(index);
}
//非常聪明非常漂亮的指针操作
private class ListItr implements ListIterator<E> {
private Node<E> lastReturned;
private Node<E> next;
private int nextIndex;
private int expectedModCount = modCount;
ListItr(int index) {
// assert isPositionIndex(index);
next = (index == size) ? null : node(index);
nextIndex = index;
}
public boolean hasNext() {
return nextIndex < size;
}
public E next() {
checkForComodification();
if (!hasNext())
throw new NoSuchElementException();
lastReturned = next;
next = next.next;
nextIndex++;
return lastReturned.item;
}
public boolean hasPrevious() {
return nextIndex > 0;
}
public E previous() {
checkForComodification();
if (!hasPrevious())
throw new NoSuchElementException();
//注意这个next是内部类里的成员变量,last是外部类的成员变量
lastReturned = next = (next == null) ? last : next.prev;
nextIndex--;
return lastReturned.item;
}
public int nextIndex() {
return nextIndex;
}
public int previousIndex() {
return nextIndex - 1;
}
public void remove() {
checkForComodification();
if (lastReturned == null)
throw new IllegalStateException();
Node<E> lastNext = lastReturned.next;
unlink(lastReturned);
if (next == lastReturned)
next = lastNext;
else
nextIndex--;
lastReturned = null;
expectedModCount++;
}
public void set(E e) {
if (lastReturned == null)
throw new IllegalStateException();
checkForComodification();
lastReturned.item = e;
}
public void add(E e) {
checkForComodification();
lastReturned = null;
if (next == null)
linkLast(e);
else
linkBefore(e, next);
nextIndex++;
expectedModCount++;
}
public void forEachRemaining(Consumer<? super E> action) {
Objects.requireNonNull(action);
while (modCount == expectedModCount && nextIndex < size) {
action.accept(next.item);
lastReturned = next;
next = next.next;
nextIndex++;
}
checkForComodification();
}
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
//节点类,平平无奇链表捏
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
public Iterator<E> descendingIterator() {
return new DescendingIterator();
}
// 2 降序迭代器
private class DescendingIterator implements Iterator<E> {
//借助升序迭代器实现
private final ListItr itr = new ListItr(size());
public boolean hasNext() {
return itr.hasPrevious();
}
public E next() {
return itr.previous();
}
public void remove() {
itr.remove();
}
}
private LinkedList<E> superClone() {
try {
return (LinkedList<E>) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
public Object clone() {
LinkedList<E> clone = superClone();
// 初始化
clone.first = clone.last = null;
clone.size = 0;
clone.modCount = 0;
// Initialize clone with our elements
for (Node<E> x = first; x != null; x = x.next)
clone.add(x.item);
return clone;
}
public Object[] toArray() {...}
public <T> T[] toArray(T[] a) {...}
private static final long serialVersionUID = 876323262645176354L;
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {...}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {...}
public Spliterator<E> spliterator() {
return new LLSpliterator<E>(this, -1, 0);
}
static final class LLSpliterator<E> implements Spliterator<E> {...}
//4
}它接下来举的例子很有意思,可惜不知道为啥在我这一直segment fault。。。好像是它用的内联汇编是32位什么的,我折腾了半天还是没办法,算了
+-
/*
我们这个例子中将实现一个更为灵活的叫做runso的程序,这个程序可以通过命令行来执行共享对象里面的任意一个函数。
它在理论上很简单,基本的步骤就是:由命令行给出共享对象路径、函数名和相关参数,然后程序通过运行时加载将该模块加载
到进程中,查找相应的函数,并且执行它,然后将执行结果打印出来。
为了表示参数和返回值类型,我们假设字母d表示double、i表示int、s表示char*、v表示void
比如说,如果要调用/lib/libfoo.so里面一个void bar(char* str, int i)的函数,可以使用如下命令行:
$./RunSo /lib/libfoo.so bar sHello i10
*/
// 大概就是根据参数类型把参数压入栈
// 大概就是相当于pop,给esp加上我们之前申请的栈空间esp
int main(int argc, char* argv[])
{
void* handle;
char* error;
int i;
int esp = 0;
void* func;
handle = dlopen(argv[1], RTLD_NOW);
if(handle == 0) {
printf("Can't find library: %s\n", argv[1]);
return -1;
}
func = dlsym(handle, argv[2]);
if( (error = dlerror()) != NULL ) {
printf("Find symbol %s error: %s\n", argv[2], error);
goto exit_runso;
}
// 根据返回值不同构造函数指针
switch(argv[argc-1][0]){
case 'i':
{
int (*func_int)() = func;
SETUP_STACK;
int ret = func_int();
RESTORE_STACK;
printf("ret = %d\n", ret );
break;
}
case 'd':
{
double (*func_double)() = func;
SETUP_STACK;
double ret = func_double();
RESTORE_STACK;
printf("ret = %f\n", ret );
break;
}
case 's':
{
char* (*func_str)() = func;
SETUP_STACK;
char* ret = func_str();
RESTORE_STACK;
printf("ret = %s\n", ret );
break;
}
case 'v':
{
void (*func_void)() = func;
SETUP_STACK;
func_void();
RESTORE_STACK;
printf("ret = void");
break;
}
} // end of switch
exit_runso:
dlclose(handle);
}其中
-
-- -
关于分代GC
In the
-clear()
function:-
// Clearing all of the links between nodes is "unnecessary", but:
// - helps a generational GC if the discarded nodes inhabit
// more than one generation
// - is sure to free memory even if there is a reachable Iterator还没看懂,插个眼
-- +
降序迭代器
一切都反过来了,也没有升序迭代器恁多方法:
-不支持foreach循环,只支持单向遍历,没有add set 只有remove。
+ +运行库
初始化
操作系统装载程序之后,首先运行的代码并不是main的第一行,而是某些别的代码,这些代码负责准备好main函数执行所需要的环境,并且负责调用main函数,这时候你才可以在main函数里放心大胆地写各种代码:申请内存、使用系统调用、触发异常、访问I/O。在main返回之后,它会记录main函数的返回值,调用atexit注册的函数,然后结束进程。
+运行这些代码的函数称为入口函数或入口点(Entry Point),视平台的不同而有不同的名字。程序的入口点实际上是一个程序的初始化和结束部分,它往往是运行库的一部分。
+一个典型的程序运行步骤大致如下:
++
-- -
操作系统在创建进程后,把控制权交到了程序的入口,这个入口往往是运行库中的某个入口函数。
关于unlinkLast/First的参数问题
-事实证明确实人家也觉得无参比较合理(
+- -
入口函数对运行库和程序运行环境进行初始化,包括堆、I/O、线程、全局变量构造,等等。
sublist
LinkedList用了从AbstractList继承来的sublist相关类和方法,没有特别的优化,其sublist不可序列化,且not cloneable。
+- -
入口函数在完成初始化之后,调用main函数,正式开始执行程序主体部分。
transient的三个成员变量
In Java LinkedList, why the “first”, “last” and “size” variable are transient?
--- -LinkedList provide its own method for serializing and de-serializing.
-When serializing, it only writes the size, and the values of the list.
-When deserializing, it reads the size, then build the list from scratch, each node for each value at a time.
-If the author did not provide their own read and write methods, then they would need to make size, first, and last non-transient. They would also need to make the Node class serializable.
---Otherwise serializing would be by default, which would be recursive, and for a large list would easily blow the stack.
-意思就是序列化时不记录这些信息,反序列化时会重新构建。还有说如果用默认的序列化方法是递归的可能爆栈?还有我觉得有一点可能是如果把所有node都序列化了,可能反序列化后,本来分配到那段内存空间要是被占用了,但指针值不变还是会有问题?等待之后解答。
+main函数执行完毕以后,返回到入口函数,入口函数进行清理⼯作,包括全局变量析构、堆销毁、关闭I/O等,然后进行系统调用结束进程。
Vector
-Unlike the new collection implementations, Vector is synchronized. If a thread-safe implementation is not needed, it is recommended to use ArrayList in place of Vector.
+Linux中的C语言运行库就是glibc。
+运行库
运行时库(Runtime Library)为入口函数及其所依赖的函数所构成的函数、各种标准库函数的实现的集合。可以通过
+sudo apt-get install glibc-source
安装glibc的源代码。一个C语言运行库大致包含了如下功能:
++
- +
启动与退出:包括入口函数及入口函数所依赖的其他函数等。
+- +
标准函数:由C语言标准规定的C语言标准库所拥有的函数实现。
+- +
I/O:I/O功能的封装和实现,参见上一节中I/O初始化部分。
+应该指的是比如说提供File*指针、IO stream之类的高级功能封装。
+堆:堆的封装和实现,参见上一节中堆初始化部分。
++-这点让我耳目一新!因为我以前一直以为堆栈都是操作系统实现的,现在想来才发现确实,操作系统只负责通过sbrk系统调用给内存,具体的堆分配算法由glibc的malloc实现。
代码:
- -
public class Vector<E>
extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
protected Object[] elementData;
protected int elementCount;
protected int capacityIncrement;
private static final long serialVersionUID = -2767605614048989439L;
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
public Vector() {
//1
this(10);
}
public Vector(Collection<? extends E> c) {
Object[] a = c.toArray();
elementCount = a.length;
if (c.getClass() == ArrayList.class) {
elementData = a;
} else {
elementData = Arrays.copyOf(a, elementCount, Object[].class);
}
}
public synchronized void copyInto(Object[] anArray) {
System.arraycopy(elementData, 0, anArray, 0, elementCount);
}
//2
public synchronized void trimToSize() {
modCount++;
int oldCapacity = elementData.length;
if (elementCount < oldCapacity) {
elementData = Arrays.copyOf(elementData, elementCount);
}
}
public synchronized void ensureCapacity(int minCapacity) {
if (minCapacity > 0) {
modCount++;
ensureCapacityHelper(minCapacity);
}
}
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
private void grow(int minCapacity) {...}
private static int hugeCapacity(int minCapacity) {...}
/*
Sets the size of this vector. If the new size is greater than the current size, new null items are added to the end of the vector. If the new size is less than the current size, all components at index newSize and greater are discarded.
*/
public synchronized void setSize(int newSize) {
modCount++;
if (newSize > elementCount) {
ensureCapacityHelper(newSize);
} else {
for (int i = newSize ; i < elementCount ; i++)
//3 GC帮大忙,注意这里没有trim。
elementData[i] = null;
}
}
elementCount = newSize;
}
//capacity 、size 、isEmpty省略
//4
public Enumeration<E> elements() {
return new Enumeration<E>() {
int count = 0;
public boolean hasMoreElements() {
return count < elementCount;
}
public E nextElement() {
synchronized (Vector.this) {
if (count < elementCount) {
return elementData(count++);
}
}
throw new NoSuchElementException("Vector Enumeration");
}
};
}
//contains、indexof、lastIndexOf省略
public synchronized E elementAt(int index) {
if (index >= elementCount) {
throw new ArrayIndexOutOfBoundsException(index + " >= " + elementCount);
}
return elementData(index);
}
public synchronized E firstElement() {
if (elementCount == 0) {
throw new NoSuchElementException();
}
return elementData(0);
}
public synchronized E lastElement() {
if (elementCount == 0) {
throw new NoSuchElementException();
}
return elementData(elementCount - 1);
}
public synchronized void setElementAt(E obj, int index) {...}
public synchronized void removeElementAt(int index) {...}
public synchronized void insertElementAt(E obj, int index) {...}
public synchronized void addElement(E obj) {...}
public synchronized boolean removeElement(Object obj) {...}
//clear
public synchronized void removeAllElements() {
modCount++;
// Let gc do its work
for (int i = 0; i < elementCount; i++)
elementData[i] = null;
elementCount = 0;
}
public synchronized Object clone() {...}
public synchronized Object[] toArray() {
return Arrays.copyOf(elementData, elementCount);
}
public synchronized <T> T[] toArray(T[] a) {...}
// Positional Access Operations
//add set get remove clear省略
// Bulk Operations
// xxxAll省略
//用了AbstractList的equal、hashcode、tostring,省略
public synchronized List<E> subList(int fromIndex, int toIndex) {
return Collections.synchronizedList(super.subList(fromIndex, toIndex),
this);
}
protected synchronized void removeRange(int fromIndex, int toIndex) {...}
//跟AL不一样
private void readObject(ObjectInputStream in)
throws IOException, ClassNotFoundException {
ObjectInputStream.GetField gfields = in.readFields();
int count = gfields.get("elementCount", 0);
Object[] data = (Object[])gfields.get("elementData", null);
if (count < 0 || data == null || count > data.length) {
throw new StreamCorruptedException("Inconsistent vector internals");
}
elementCount = count;
elementData = data.clone();
}
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
final java.io.ObjectOutputStream.PutField fields = s.putFields();
final Object[] data;
synchronized (this) {
fields.put("capacityIncrement", capacityIncrement);
fields.put("elementCount", elementCount);
data = elementData.clone();
}
fields.put("elementData", data);
s.writeFields();
}
public synchronized ListIterator<E> listIterator(int index) {
if (index < 0 || index > elementCount)
throw new IndexOutOfBoundsException("Index: "+index);
return new ListItr(index);
}
public synchronized ListIterator<E> listIterator() {
return new ListItr(0);
}
public synchronized Iterator<E> iterator() {
return new Itr();
}
private class Itr implements Iterator<E> {...}
final class ListItr extends Itr implements ListIterator<E> {...}
public synchronized void forEach(Consumer<? super E> action) {...}
public synchronized boolean removeIf(Predicate<? super E> filter) {...}
public synchronized void replaceAll(UnaryOperator<E> operator) {...}
public synchronized void sort(Comparator<? super E> c) {...}
public Spliterator<E> spliterator() {
return new VectorSpliterator<>(this, null, 0, -1, 0);
}
static final class VectorSpliterator<E> implements Spliterator<E> {...}
}其中:
-
-- -
默认容量
空构造器Vector()创建出来的默认容量为10,不同于ArrayList是个空集合。
扩容操作
与ArrayList基本上是雷同的,就是都是synchronized。
+- -
语言实现:语言中一些特殊功能的实现。
setsize不改变容量
实现中只是把东西设置为空,并没有trim,因而容量不变
+- -
调试:实现调试功能的代码。
Vector可生成枚举类
Set
Set(I)
代码:
+
/* 1
Note: 当set里包含可变的对象时,要多加小心。
The behavior of a set is not specified if the value of a set object is changed in a manner that affects equals comparisons.
A special case of this prohibition is that it is not permissible for a set to contain itself as an element.
*/
public interface Set<E> extends Collection<E> {
// Query Operations
int size();
boolean isEmpty();
boolean contains(Object o);
/*Returns an iterator over the elements in this set.
The elements are returned in no particular order
(unless this set is an instance of some class that provides a guarantee).*/
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
// Modification Operations
/*
If this set already contains the element,
the call leaves the set unchanged and returns false.
*/
boolean add(E e);
boolean remove(Object o);
// Bulk Operations
/*
Returns true if this set contains all of the elements of the specified collection.
If the specified collection is also a set,
this method returns true if it is a subset of this set.[这个subset的定义有点意思]
*/
boolean containsAll(Collection<?> c);
//2
//取并集
boolean addAll(Collection<? extends E> c);
//取交集
boolean retainAll(Collection<?> c);
//集合差
boolean removeAll(Collection<?> c);
void clear();
// Comparison and hashing
boolean equals(Object o);
int hashCode();
default Spliterator<E> spliterator() {
return Spliterators.spliterator(this, Spliterator.DISTINCT);
}
}库函数介绍
它这里主要讲了两个比较特殊的库,还挺有意思的:变长参数(stdarg.h)和非局部跳转(setjmp.h)。
++
变长参数
+讲这玩意其实用作是printf的实现。看下下面这两个代码相信你就能明白printf的基本原理了:
+-
// Code 1
int sum(unsigned num, ...)
{
int* p = &num + 1;
int ret = 0;
while (num--)
ret += *p++;
return ret;
}
call:
int n = sum(3, 16, 38, 53);其中:
-
+- +
set中包含可变对象
需要规避可修改对象,使其与集合中另一个元素重复的问题
-详见此:
-Java HashSet contains duplicates if contained element is modified
++ +
// Code 2
void arg_match(const char* fmt, ...) {
va_list ap; // 本质char * / void *
va_start(ap, fmt); // 之后ap就会指向fmt后的第一个可变参数
int idx = 0;
for (int i = 0; i < strlen(fmt); i ++) {
if (fmt[i] != '%') continue;
idx ++;
switch (fmt[i + 1]) {
case 'd':
int argv_i = va_arg(ap, int);
printf("第%d个参数为:%d\n", idx, argv_i);
break;
case 's':
char* argv_s = va_arg(ap, char*);
printf("第%d个参数为:%s\n", idx, argv_s);
break;
default:
printf("unknown.\n");
break;
}
}
}
call:
arg_match("%d %d %s\n", 1, 2, "333");除此之外,我们也可以实现变长参数宏:
+在GCC编译器下,变长参数宏可以使用“##”宏字符串连接操作实现。
+
- -
非局部跳转
+这位更是重量级
++ +
jmp_buf b;
void f()
{
longjmp(b, 1);
}
int main()
{
if (setjmp(b))
printf("World!");
else
{
printf("Hello ");
f();
}
}事实上的输出是:
+Hello World!
-The correct solution is to stick to the contract of
-Set
and not modify objects after adding them to the collection.You can avoid this problem by either:
--
-- using an immutable type for your set elements,
-- making a copy of the objects as you put them into the set and / or pull them out of the set,
-- writing your code so that it “knows“ not to change the objects for the duration …
-From the perspective of correctness and robustness, the first option is clearly best.
+实际上,当setjmp正常返回的时候,会返回0,因此会打印出“Hello”的字样。而longjmp的作用,就是让程序的执行流回到当初setjmp返回的时刻,并且返回由longjmp指定的返回值(longjmp的参数2),也就是1,自然接着会打印出“World!”并退出。换句话说,longjmp可以让程序“时光倒流”回setjmp返回的时刻,并改变其行为,以至于改变了未来。
集合操作
set抽象自数学的集合,因此有很多对应的集合操作:
+glibc
glibc是对标准C运行库的扩展(如增加了pthread),全称GNU C Library,是GNU旗下的C标准库。
+生命周期
于是,我们可以完整串联整个运行程序的生命周期:
+由链接器ld将所有.o文件的
+_init
段和_finit
段(包含glibc对堆空间的初始化和释放、编译器对C++全局对象构造析构的实现以及app自己实现的init和finit函数)分别串在一起,并且链接上glibc库的包含了_start
(会调用_init
)的crt.o文件,最后就形成了包含各种glibc标准库和真·用户代码的可执行文件。可执行文件被装载到进程地址空间后,首先会进行动态链接。然后,从程序入口
+_start
开始进行各种初始化,调用可执行文件的这个_init
段的内容。init完成之后,glibc就调用程序中的入口main。main执行过程中会用到glibc的各种标准库函数。main执行完后就会继续执行_finit
段来结束一切。C++的全局对象构造析构
+
构造(
+_init
)编译器会将每个全局对象的构造函数以如下形式包装:
++ +
static void GLOBAL__I_Hw(void)
{
Hw::Hw(); // 构造对象
atexit(__tcf_1); // 一个神秘的函数叫做__tcf_1被注册到了exit
}然后将这个
+GLOBAL__I_Hw
放进.o文件的一个.ctor
段中,最后由ld将各个.o文件的.ctor
段链接起来,并计算出全局对象数量填入crtbegin.o
即可。之后在
_init
段中遍历.ctor
的各个函数指针进行构造函数调用就行了-+addAll ∪
-retainAll ∩
-removeAll -
-后日谈:今天又在rtt中看到了这一牛掰操作。rtt也是大概通过这个原理实现的帅的一匹的“Automatic Initialization Mechanism”。
+原理感觉也是将其放入一个特殊的”rti_fn$f”段,并且用rti_start和end来标识该段结束,
++ +
// xiunian: INIT_EXPORT应该是这个
INIT_EXPORT(fn, "1.0") 宏展开:
const char __rti_level_fn[] = ".rti_fn." "1.0";
// 指示编译器将特定的变量或数据结构分配到名为 "rti_fn$f" 的内存段(Memory Segment)中
__declspec(allocate("rti_fn$f"))
rt_used const struct rt_init_desc __rt_init_msc_fn = {__rti_level_fn, fn };+ +
/*
* xiunian: 牛逼,这里颇有学链接时的感觉了
* 这里介绍了组件初始化顺序
* Components Initialization will initialize some driver and components as following
* order:
* rti_start --> 0
* BOARD_EXPORT --> 1
* rti_board_end --> 1.end
*
* DEVICE_EXPORT --> 2
* COMPONENT_EXPORT --> 3
* FS_EXPORT --> 4
* ENV_EXPORT --> 5
* APP_EXPORT --> 6
*
* rti_end --> 6.end
*
* These automatically initialization, the driver or component initial function must
* be defined with:
* INIT_BOARD_EXPORT(fn);
* INIT_DEVICE_EXPORT(fn);
* ...
* INIT_APP_EXPORT(fn);
* etc.
*/
static int rti_start(void)
{
return 0;
}
INIT_EXPORT(rti_start, "0");
static int rti_board_start(void)
{
return 0;
}
INIT_EXPORT(rti_board_start, "0.end");
static int rti_board_end(void)
{
return 0;
}
INIT_EXPORT(rti_board_end, "1.end");
static int rti_end(void)
{
return 0;
}
INIT_EXPORT(rti_end, "6.end");之后真正初始化只需遍历然后调用函数指针即可
+
void rt_components_board_init(void)
{
volatile const init_fn_t *fn_ptr;
for (fn_ptr = &__rt_init_rti_board_start; fn_ptr < &__rt_init_rti_board_end; fn_ptr++)
{
(*fn_ptr)();
}
}析构(
+_finit
)早期同理可得。现在变了,变成直接在
+GLOBAL__I_Hw
中注册atexit
了。
static void __tcf_1(void) //这个名字由编译器生成
{
Hw.~HelloWorld();
}AbstratcSet(A)
-Note that this class does not override any of the implementations from the AbstractCollection class. It merely adds implementations for equals and hashCode.
+实现小型运行库
+-看到标题就知道接下来有多帅了
代码:
+
public abstract class AbstractSet<E> extends AbstractCollection<E> implements Set<E> {
protected AbstractSet() {
}
// Comparison and hashing
public boolean equals(Object o) {
if (o == this)
return true;
if (!(o instanceof Set))
return false;
Collection<?> c = (Collection<?>) o;
if (c.size() != size())
return false;
try {
return containsAll(c);
} catch (ClassCastException unused) {
return false;
} catch (NullPointerException unused) {
return false;
}
}
public int hashCode() {
int h = 0;
Iterator<E> i = iterator();
while (i.hasNext()) {
E obj = i.next();
if (obj != null)
h += obj.hashCode();
}
return h;
}
//不大明白为啥要修改实现,用AbstractCollection的不好吗
public boolean removeAll(Collection<?> c) {
Objects.requireNonNull(c);
boolean modified = false;
if (size() > c.size()) {
for (Iterator<?> i = c.iterator(); i.hasNext(); )
modified |= remove(i.next());
} else {
for (Iterator<?> i = iterator(); i.hasNext(); ) {
if (c.contains(i.next())) {
i.remove();
modified = true;
}
}
}
return modified;
}
}在这一章我们仅实现CRT几个关键的部分。虽然这个迷你CRT仅仅实现了为数不多的功能,但是它已经具备了CRT的关键功能:入口函数、初始化、堆管理、基本IO,甚至还将实现堆C++的new/delete、stream和string的支持。
+本章主要分为两个部分,首先实现一个仅仅支持C语言的运行库,即传统意义上的CRT。其次,将为这个CRT加入一部分以支持C++语言的运行时特性。
+相关代码放在github了,其实感觉差不多是按它写的抄了一遍。可以现在稍微整理下文件结构。
++
- +
just for C
+前面说到,CRT的作用是执行init和finit段、进行堆的管理、进行IO的封装管理以及提供各种标准C语言库。因而,我们可以分别用如下几个文件来实现这几个功能:
++
+- +
+
entry.c
用于实现入口函数
+mini_crt_entry
。入口函数中主要要做:调用main之前的栈构造、堆初始化、IO初始化,最后调用main函数。main函数返回后,通过系统调用exit来杀死进程。- +
+
malloc.c
用于实现堆的管理,主要实现了
+malloc
和free
。使用了空闲链表的小内存管理法,实现简单。- +
+
stdio.c
用于实现IO封装,
+fread
、fwrite
、fopen
、fclose
、fseek
。实现简单,因而只是系统调用的封装- +
+
string.c
以字符串操作为例,提供的标准C语言库。
+之后,我们将其以如下参数编译为静态库:
+-
$ gcc -c -fno-builtin -nostdlib -fno-stack-protector entry.c malloc.c stdio.c string.c printf.c
$ ar -rs minicrt.a malloc.o printf.o stdio.o string.o
# 编译测试用例
$ gcc -m32 -c -ggdb -fno-builtin -nostdlib -fno-stack-protector test.cHashSet
--In particular, it does not guarantee that the order will remain constant over time.
-Iterating over this set requires time proportional to the sum of the HashSet instance’s size (the number of elements) plus the “capacity” of the backing HashMap instance (the number of buckets).
-Thus, it’s very important not to set the initial capacity too high (or the load factor too low) if iteration performance is important.
-This implementation is not synchronized.
-Set s = Collections.synchronizedSet(new HashSet(...));
代码:
+
public class HashSet<E>
extends AbstractSet<E>
implements Set<E>, Cloneable, java.io.Serializable
{
static final long serialVersionUID = -5024744406713321676L;
//通过hashmap实现
//map不可序列化
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
// 1
private static final Object PRESENT = new Object();
//Constructs a new, empty set;
//the backing HashMap instance has default initial capacity (16)
//and load factor (0.75).
public HashSet() {
map = new HashMap<>();
}
//The HashMap is created with default load factor (0.75)
//and an initial capacity sufficient to contain the elements in c
public HashSet(Collection<? extends E> c) {
//看来这个load factor=size/0.75
map = new HashMap<>(Math.max((int) (c.size()/.75f) + 1, 16));
addAll(c);
}
public HashSet(int initialCapacity, float loadFactor) {
map = new HashMap<>(initialCapacity, loadFactor);
}
public HashSet(int initialCapacity) {
map = new HashMap<>(initialCapacity);
}
//This package private constructor is only used by LinkedHashSet.
//@param: dummy – ignored (distinguishes this constructor from other constructor.)
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
//此处为LinkeHashMap
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
public Iterator<E> iterator() {return map.keySet().iterator();}
public int size() {return map.size();}
public boolean isEmpty() {return map.isEmpty();}
public boolean contains(Object o) {return map.containsKey(o);}
//@return true if this set did not already contain the specified element
//map.put返回已有的oldValue,返回空表示没有oldValue,插入成功;否则失败
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}
public void clear() {
map.clear();
}
public Object clone() {
try {
HashSet<E> newSet = (HashSet<E>) super.clone();
newSet.map = (HashMap<E, Object>) map.clone();
return newSet;
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
}
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {...}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {...}
public Spliterator<E> spliterator() {
return new HashMap.KeySpliterator<E,Object>(map, 0, -1, 0, 0);
}
}再指定
+mini_crt_entry
为入口进行静态链接:
$ ld -m elf_i386 -static -e mini_crt_entry entry.o test.o minicrt.a -o testC++
+如果要实现对C++的支持,除了在上述基础上,我们还需增加以下几个内容:全局对象(cout)构造/析构的实现、new/delete、类的实现(string和iostream)。具体来说,会支持下面这个简单的代码:
+-
using namespace std;
int main(int argc, char* argv[])
{
string* msg = new string("Hello World");
cout << *msg << endl;
delete msg;
return 0;
}其中:
-
-- -
PRESENT
正如它的解释:
---Dummy value to associate with an Object in the backing Map
-set 以map作为内部支持,其实主要用的是map对于key的高效去重。也就是说,set其实只需要用map的key这一半就好了。所以我们另一半value就都统一用一个new Object【也就是PRESENT】来统一就行。
-不得不说这点很聪明,值得学习。
-
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return map.remove(o)==PRESENT;
}SortedSet(I)
--A Set that further provides a total ordering on its elements.
-The set’s iterator will traverse the set in ascending element order.
-All elements inserted into a sorted set must implement the Comparable interface (or be accepted by the specified comparator).否则导致ClassCastException
-Note that the ordering maintained by a sorted set (whether or not an explicit comparator is provided) must be consistent with equals if the sorted set is to correctly implement the Set interface.
-【consistent with equals:if and only if c.compare(e1, e2)==0 has the same boolean value as e1.equals(e2) for every e1 and e2 in S.】
-//2
-Note: several methods return subsets with restricted ranges. 区间是前闭后开的。
-If you need a closed range , and the element type allows for calculation of the successor of a given value, merely request the subrange from lowEndpoint to successor(highEndpoint).
-For example, suppose that s is a sorted set of strings. [low,hight]
-
SortedSet<String> sub = s.subSet(low, high+"\0");
A similar technique can be used to generate an open range (low,hight)
SortedSet<String> sub = s.subSet(low+"\0", high);
66666
-代码
+
public interface SortedSet<E> extends Set<E> {
//null if this set uses the natural ordering
Comparator<? super E> comparator();
//1
SortedSet<E> subSet(E fromElement, E toElement);
SortedSet<E> headSet(E toElement);
SortedSet<E> tailSet(E fromElement);
E first();
E last();
default Spliterator<E> spliterator() {
return new Spliterators.IteratorSpliterator<E>(
this, Spliterator.DISTINCT | Spliterator.SORTED | Spliterator.ORDERED) {
public Comparator<? super E> getComparator() {
return SortedSet.this.comparator();
}
};
}
}我们可以分步实现这些功能:
++
+- +
new/delete实现
+简单地使用运算符重载功能即可:
+
void* operator new(unsigned int size);
void operator delete(void* p);- +
类的实现
+不多说
+- +
全局对象的构造/析构
++
+- +
构造
+全局对象的构造在entry中进行:
+-
void mini_crt_entry(void)
{
...
// 构造所有全局对象
do_global_ctors();
ret = main(argc,argv);
}其中:
-
-- -
subset
只有sorted set才有subset,想想也确实
-+
/*
Throws:
ClassCastException – if fromElement and toElement cannot be compared to one another using this set's comparator
NullPointerException – if fromElement or toElement is null and this set does not permit null elements
IllegalArgumentException – if fromElement is greater than toElement; or fromElement or toElement lies outside the bounds of the restricted range of the set
*/
//The returned set will throw an IllegalArgumentException
//on an attempt to insert an element outside its range.
SortedSet<E> subSet(E fromElement, E toElement);前文说过,在Linux中,每个.o文件的全局构造最后都会放在
+.ctor
段。ld在链接阶段中将所有目标文件(包括用于标识.ctor
段开始和结束的crtbegin.o
和crtend.o
)的.ctor
段连在一起。所以,我们就需要实现三个文件:+
- +
+
ctors.c
主要是用于实现
+do_global_ctors()
。既然都有.ctor
段存在了,那么它的实现就很简单,就是遍历.ctor
段的所有函数指针并且调用它们。-
void run_hooks();
extern "C" void do_global_ctors()
{
run_hooks();
}以及注意此处是Element,不是Index
+
void run_hooks()
{
const ctor_func *list = ctors_begin;
// 逐个调用ctors段里的东西
while ((int)*++list != -1) (**list)();
}- +
+
crtbegin.c
前文说到,按规定,ld将会以如下顺序连接.o文件:
++ +
ld crtbegin.o 其他文件 crtend.o -o test因而,
+crtbegin.c
的.ctor
段会被链接在第一个。其作用是标识.ctor
函数指针的数量,将在链接时由ld计算并且填写。因而在这里,我们只需将其初始化为一个特殊值(-1)就行:
typedef void (*ctor_func)(void);
ctor_func ctors_begin[1] __attribute__((section(".ctors"))) = {
(ctor_func)-1
};- +
+
crtend.c
同样,
+crtend.c
的.ctor
段标识着.ctor
段的结束。因而我们也将其初始化为一个特殊值(-1):
typedef void (*ctor_func)(void);
// 转化-1为函数指针,标识结束
ctor_func crt_end[1] __attribute__((section(".ctors"))) = {
(ctor_func) - 1
};subSet(low, high+”\0”);
为什么加个”\0”就可以,具体可以看看这个:
-Adding “\0” to a subset range end
-原因就是sub的这个range取的是在此区间的元素,low和high这两个param不一定要包含在这个set里面。因此,按照set的排序,high+”\0”比high大,因而high就落入此区间,也就可以被包含在range中了。
+析构
+全局对象的析构同样在entry中进行:
++ +
void mini_crt_entry(void)
{
...
ret = main(argc,argv);
exit(ret);
}
void exit(int exitCode)
{
// 执行atexit,完成所有finit钩子
mini_crt_call_exit_routine();
// 调用exit系统调用
asm( "movl %0,%%ebx \n\t"
"movl $1,%%eax \n\t"
"int $0x80 \n\t"
"hlt \n\t"::"m"(exitCode));
}具体也是以链表形式管理所有的函数指针,在
atexit
中注册(加入链表),在mini_crt_call_exit_routine
中真正调用,不多分析。NavigableSet(I)
--比起sorted set,navigable set最特殊的点在于它提供了对某一元素附近元素的导航。
-Method usage
-lower less than最大的,比所给ele小的元素
-floor less than or equal最大的,比所给ele小或者等于的元素
-ceiling greater than or equal最小的,比所给ele大或者等于的元素
-higher greater than最小的,比所给ele大的元素
-The descendingSet method returns a view of the set with the senses of all relational and directional methods inverted.
-This interface additionally defines methods pollFirst and pollLast that return and remove the lowest and highest element, if one exists, else returning null. 有点堆的感觉
-Methods subSet, headSet, and tailSet differ from the like-named SortedSet methods in accepting additional arguments describing whether lower and upper bounds are inclusive versus exclusive.
-代码:
- -
public interface NavigableSet<E> extends SortedSet<E> {
//Returns the greatest element in this set strictly less than the given element
E lower(E e);
E floor(E e);
E ceiling(E e);
E higher(E e);
//Removes the first (lowest) element, or returns null if this set is empty.
E pollFirst();
E pollLast();
//in ascending order
Iterator<E> iterator();
/*
The returned set has an ordering equivalent to
Collections.reverseOrder(comparator()).
The expression s.descendingSet().descendingSet()
returns a view of s essentially equivalent(基本等价) to s
*/
NavigableSet<E> descendingSet();
//Equivalent in effect to descendingSet().iterator()
Iterator<E> descendingIterator();
NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
E toElement, boolean toInclusive);
NavigableSet<E> headSet(E toElement, boolean inclusive);
NavigableSet<E> tailSet(E fromElement, boolean inclusive);
SortedSet<E> subSet(E fromElement, E toElement);
SortedSet<E> headSet(E toElement);
SortedSet<E> tailSet(E fromElement);
}TreeSet
-This implementation provides guaranteed log(n) time cost for the basic operations (add, remove and contains).
-Note that this implementation is not synchronized.
-+
SortedSet s = Collections.synchronizedSortedSet(new TreeSet(...));
特辑:开发中遇到的链接小问题
+
]]>- + +
已经在
+LD_LIBRARY_PATH
中加入某个静态库的路径,但是仍然报错error while loading shared libraries: libssl.so.1.1: cannot open shared object file: No such file or directory
解决方法:执行
+sudo ldconfig
+-+
ldconfig
命令的作用就是将这些共享库的路径添加到动态链接器的缓存中,以便系统能够找到它们。具体而言,
+ldconfig
会检查默认的共享库路径(通常是/lib
和/usr/lib
),以及在/etc/ld.so.conf
和/etc/ld.so.conf.d/
目录中定义的其他路径。然后,它会更新动态链接器缓存,这样系统就知道在哪里查找共享库。Q:也就是说ld不是像gcc找头文件,是根据-I选项规定的路径即时查找的,而是只根据缓存吗?所以尽管我们通过ld path环境变量设置了新查找路径,我们还是得手动刷新下ld缓存。
+A:是的,你理解得很对。
+ldconfig
主要是更新系统中的共享库缓存,而不是在每次程序运行时实时查找库文件的位置。动态链接器在运行时会根据缓存中的信息找到所需的共享库,以提高性能并避免在每次程序启动时重新搜索所有库。当你通过
LD_LIBRARY_PATH
环境变量设置新的查找路径时,这只是告诉动态链接器在运行时应该额外搜索这些路径。然而,为了确保系统广泛地认识这些新的路径,以及其中的共享库,你需要运行ldconfig
来更新缓存。代码:
- +
public class TreeSet<E> extends AbstractSet<E>
implements NavigableSet<E>, Cloneable, java.io.Serializable
{
//依然用了个map
private transient NavigableMap<E,Object> m;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
TreeSet(NavigableMap<E,Object> m) {
this.m = m;
}
public TreeSet() {
this(new TreeMap<E,Object>());
}
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
public TreeSet(Collection<? extends E> c) {
this();
addAll(c);
}
public TreeSet(SortedSet<E> s) {
this(s.comparator());
addAll(s);
}
public Iterator<E> iterator() {
return m.navigableKeySet().iterator();
}
public Iterator<E> descendingIterator() {
return m.descendingKeySet().iterator();
}
//确实直接让map倒序就可以了
public NavigableSet<E> descendingSet() {
return new TreeSet<>(m.descendingMap());
}
public int size() {
return m.size();
}
public boolean isEmpty() {
return m.isEmpty();
}
public boolean contains(Object o) {
return m.containsKey(o);
}
public boolean add(E e) {
return m.put(e, PRESENT)==null;
}
public boolean remove(Object o) {
return m.remove(o)==PRESENT;
}
public void clear() {
m.clear();
}
public boolean addAll(Collection<? extends E> c) {
// Use linear-time version if applicable
if (m.size()==0 && c.size() > 0 &&
c instanceof SortedSet &&
m instanceof TreeMap) {
SortedSet<? extends E> set = (SortedSet<? extends E>) c;
TreeMap<E,Object> map = (TreeMap<E, Object>) m;
Comparator<?> cc = set.comparator();
Comparator<? super E> mc = map.comparator();
if (cc==mc || (cc != null && cc.equals(mc))) {
map.addAllForTreeSet(set, PRESENT);
return true;
}
}
return super.addAll(c);
}
public NavigableSet<E> subSet(E fromElement, boolean fromInclusive,
E toElement, boolean toInclusive) {
return new TreeSet<>(m.subMap(fromElement, fromInclusive,
toElement, toInclusive));
}
public NavigableSet<E> headSet(E toElement, boolean inclusive) {
return new TreeSet<>(m.headMap(toElement, inclusive));
}
public NavigableSet<E> tailSet(E fromElement, boolean inclusive) {
return new TreeSet<>(m.tailMap(fromElement, inclusive));
}
public SortedSet<E> subSet(E fromElement, E toElement) {
return subSet(fromElement, true, toElement, false);
}
public SortedSet<E> headSet(E toElement) {
return headSet(toElement, false);
}
//注意此处为true。严格遵循左闭右开
public SortedSet<E> tailSet(E fromElement) {
return tailSet(fromElement, true);
}
public Comparator<? super E> comparator() {
return m.comparator();
}
public E first() {
return m.firstKey();
}
public E last() {
return m.lastKey();
}
// NavigableSet API methods
public E lower(E e) {
return m.lowerKey(e);
}
public E floor(E e) {
return m.floorKey(e);
}
public E ceiling(E e) {
return m.ceilingKey(e);
}
public E higher(E e) {
return m.higherKey(e);
}
public E pollFirst() {
Map.Entry<E,?> e = m.pollFirstEntry();
return (e == null) ? null : e.getKey();
}
public E pollLast() {
Map.Entry<E,?> e = m.pollLastEntry();
return (e == null) ? null : e.getKey();
}
public Object clone() {
TreeSet<E> clone;
try {
clone = (TreeSet<E>) super.clone();
} catch (CloneNotSupportedException e) {
throw new InternalError(e);
}
clone.m = new TreeMap<>(m);
return clone;
}
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {...}
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {...}
public Spliterator<E> spliterator() {
return TreeMap.keySpliteratorFor(m);
}
private static final long serialVersionUID = -2479143000061671589L;
}