AIRobot

AIRobot quick note


  • 首页

  • 关于

  • 标签

  • 分类

  • 归档

  • 搜索

网卡驱动收包过程

发表于 2020-06-17
本文字数: 8.7k 阅读时长 ≈ 8 分钟

本文将介绍在Linux系统中,数据包是如何一步一步从网卡传到进程手中的。

如果英文没有问题,强烈建议阅读后面参考里的两篇文章,里面介绍的更详细。

本文只讨论以太网的物理网卡,不涉及虚拟设备,并且以一个UDP包的接收过程作为示例.

本示例里列出的函数调用关系来自于kernel 3.13.0,如果你的内核不是这个版本,函数名称和相关路径可能不一样,但背后的原理应该是一样的(或者有细微差别)

网卡到内存

网卡需要有驱动才能工作,驱动是加载到内核中的模块,负责衔接网卡和内核的网络模块,驱动在加载的时候将自己注册进网络模块,当相应的网卡收到数据包时,网络模块会调用相应的驱动程序处理数据。

下图展示了数据包(packet)如何进入内存,并被内核的网络模块开始处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

+-----+
| | Memroy
+--------+ 1 | | 2 DMA +--------+--------+--------+--------+
| Packet |-------->| NIC |------------>| Packet | Packet | Packet | ...... |
+--------+ | | +--------+--------+--------+--------+
| |<--------+
+-----+ |
| +---------------+
| |
3 | Raise IRQ | Disable IRQ
| 5 |
| |
↓ |
+-----+ +------------+
| | Run IRQ handler | |
| CPU |------------------>| NIC Driver |
| | 4 | |
+-----+ +------------+
|
6 | Raise soft IRQ
|
↓
  • 1: 数据包从外面的网络进入物理网卡。如果目的地址不是该网卡,且该网卡没有开启混杂模式,该包会被网卡丢弃。

  • 2: 网卡将数据包通过DMA的方式写入到指定的内存地址,该地址由网卡驱动分配并初始化。注: 老的网卡可能不支持DMA,不过新的网卡一般都支持。

  • 3: 网卡通过硬件中断(IRQ)通知CPU,告诉它有数据来了

  • 4: CPU根据中断表,调用已经注册的中断函数,这个中断函数会调到驱动程序(NIC Driver)中相应的函数

  • 5: 驱动先禁用网卡的中断,表示驱动程序已经知道内存中有数据了,告诉网卡下次再收到数据包直接写内存就可以了,不要再通知CPU了,这样可以提高效率,避免CPU不停的被中断。

  • 6: 启动软中断。这步结束后,硬件中断处理函数就结束返回了。由于硬中断处理程序执行的过程中不能被中断,所以如果它执行时间过长,会导致CPU没法响应其它硬件的中断,于是内核引入软中断,这样可以将硬中断处理函数中耗时的部分移到软中断处理函数里面来慢慢处理。

    内核的网络模块

软中断会触发内核网络模块中的软中断处理函数,后续流程如下

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

+-----+
17 | |
+----------->| NIC |
| | |
|Enable IRQ +-----+
|
|
+------------+ Memroy
| | Read +--------+--------+--------+--------+
+--------------->| NIC Driver |<--------------------- | Packet | Packet | Packet | ...... |
| | | 9 +--------+--------+--------+--------+
| +------------+
| | | skb
Poll | 8 Raise softIRQ | 6 +-----------------+
| | 10 |
| ↓ ↓
+---------------+ Call +-----------+ +------------------+ +--------------------+ 12 +---------------------+
| net_rx_action |<-------| ksoftirqd | | napi_gro_receive |------->| enqueue_to_backlog |----->| CPU input_pkt_queue |
+---------------+ 7 +-----------+ +------------------+ 11 +--------------------+ +---------------------+
| | 13
14 | + - - - - - - - - - - - - - - - - - - - - - - +
↓ ↓
+--------------------------+ 15 +------------------------+
| __netif_receive_skb_core |----------->| packet taps(AF_PACKET) |
+--------------------------+ +------------------------+
|
| 16
↓
+-----------------+
| protocol layers |
+-----------------+
  • 7: 内核中的ksoftirqd进程专门负责软中断的处理,当它收到软中断后,就会调用相应软中断所对应的处理函数,对于上面第6步中是网卡驱动模块抛出的软中断,ksoftirqd会调用网络模块的net_rx_action函数
  • 8: net_rx_action调用网卡驱动里的poll函数来一个一个的处理数据包
  • 9: 在pool函数中,驱动会一个接一个的读取网卡写到内存中的数据包,内存中数据包的格式只有驱动知道
  • 10: 驱动程序将内存中的数据包转换成内核网络模块能识别的skb格式,然后调用napi_gro_receive函数
  • 11: napi_gro_receive会处理GRO相关的内容,也就是将可以合并的数据包进行合并,这样就只需要调用一次协议栈。然后判断是否开启了RPS,如果开启了,将会调用enqueue_to_backlog
  • 12: 在enqueue_to_backlog函数中,会将数据包放入CPU的softnet_data结构体的input_pkt_queue中,然后返回,如果input_pkt_queue满了的话,该数据包将会被丢弃,queue的大小可以通过net.core.netdev_max_backlog来配置
  • 13: CPU会接着在自己的软中断上下文中处理自己input_pkt_queue里的网络数据(调用__netif_receive_skb_core)
  • 14: 如果没开启RPS,napi_gro_receive会直接调用__netif_receive_skb_core
  • 15: 看是不是有AF_PACKET类型的socket(也就是我们常说的原始套接字),如果有的话,拷贝一份数据给它。tcpdump抓包就是抓的这里的包。
  • 16: 调用协议栈相应的函数,将数据包交给协议栈处理。
  • 17: 待内存中的所有数据包被处理完成后(即poll函数执行完成),启用网卡的硬中断,这样下次网卡再收到数据的时候就会通知CPU
    enqueue_to_backlog函数也会被netif_rx函数调用,而netif_rx正是lo设备发送数据包时调用的函数
    协议栈

IP层

由于是UDP包,所以第一步会进入IP层,然后一级一级的函数往下调:

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
          |
|
↓ promiscuous mode &&
+--------+ PACKET_OTHERHOST (set by driver) +-----------------+
| ip_rcv |-------------------------------------->| drop this packet|
+--------+ +-----------------+
|
|
↓
+---------------------+
| NF_INET_PRE_ROUTING |
+---------------------+
|
|
↓
+---------+
| | enabled ip forword +------------+ +----------------+
| routing |-------------------->| ip_forward |------->| NF_INET_FORWARD |
| | +------------+ +----------------+
+---------+ |
| |
| destination IP is local ↓
↓ +---------------+
+------------------+ | dst_output_sk |
| ip_local_deliver | +---------------+
+------------------+
|
|
↓
+------------------+
| NF_INET_LOCAL_IN |
+------------------+
|
|
↓
+-----------+
| UDP layer |
+-----------+
  • ip_rcv: ip_rcv函数是IP模块的入口函数,在该函数里面,第一件事就是将垃圾数据包(目的mac地址不是当前网卡,但由于网卡设置了混杂模式而被接收进来)直接丢掉,然后调用注册在NF_INET_PRE_ROUTING上的函数

  • NF_INET_PRE_ROUTING: netfilter放在协议栈中的钩子,可以通过iptables来注入一些数据包处理函数,用来修改或者丢弃数据包,如果数据包没被丢弃,将继续往下走

  • routing: 进行路由,如果是目的IP不是本地IP,且没有开启ip forward功能,那么数据包将被丢弃,如果开启了ip forward功能,那将进入ip_forward函数

  • ip_forward: ip_forward会先调用netfilter注册的NF_INET_FORWARD相关函数,如果数据包没有被丢弃,那么将继续往后调用dst_output_sk函数

  • dst_output_sk: 该函数会调用IP层的相应函数将该数据包发送出去,同下一篇要介绍的数据包发送流程的后半部分一样。

  • ip_local_deliver:如果上面routing的时候发现目的IP是本地IP,那么将会调用该函数,在该函数中,会先调用NF_INET_LOCAL_IN相关的钩子程序,如果通过,数据包将会向下发送到UDP层

    UDP层

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
             |
    |
    ↓
    +---------+ +-----------------------+
    | udp_rcv |----------->| __udp4_lib_lookup_skb |
    +---------+ +-----------------------+
    |
    |
    ↓
    +--------------------+ +-----------+
    | sock_queue_rcv_skb |----->| sk_filter |
    +--------------------+ +-----------+
    |
    |
    ↓
    +------------------+
    | __skb_queue_tail |
    +------------------+
    |
    |
    ↓
    +---------------+
    | sk_data_ready |
    +---------------+
  • udp_rcv: udp_rcv函数是UDP模块的入口函数,它里面会调用其它的函数,主要是做一些必要的检查,其中一个重要的调用是__udp4_lib_lookup_skb,该函数会根据目的IP和端口找对应的socket,如果没有找到相应的socket,那么该数据包将会被丢弃,否则继续

  • sock_queue_rcv_skb: 主要干了两件事,一是检查这个socket的receive buffer是不是满了,如果满了的话,丢弃该数据包,然后就是调用sk_filter看这个包是否是满足条件的包,如果当前socket上设置了filter,且该包不满足条件的话,这个数据包也将被丢弃(在Linux里面,每个socket上都可以像tcpdump里面一样定义filter,不满足条件的数据包将会被丢弃)

  • __skb_queue_tail: 将数据包放入socket接收队列的末尾

  • sk_data_ready: 通知socket数据包已经准备好

  • 调用完sk_data_ready之后,一个数据包处理完成,等待应用层程序来读取,上面所有函数的执行过程都在软中断的上下文中。

    socket

应用层一般有两种方式接收数据,一种是recvfrom函数阻塞在那里等着数据来,这种情况下当socket收到通知后,recvfrom就会被唤醒,然后读取接收队列的数据;另一种是通过epoll或者select监听相应的socket,当收到通知后,再调用recvfrom函数去读取接收队列的数据。两种情况都能正常的接收到相应的数据包。

结束语

了解数据包的接收流程有助于帮助我们搞清楚我们可以在哪些地方监控和修改数据包,哪些情况下数据包可能被丢弃,为我们处理网络问题提供了一些参考,同时了解netfilter中相应钩子的位置,对于了解iptables的用法有一定的帮助,同时也会帮助我们后续更好的理解Linux下的网络虚拟设备。

在接下来的几篇文章中,将会介绍Linux下的网络虚拟设备和iptables。

参考

Monitoring and Tuning the Linux Networking Stack: Receiving Data
Illustrated Guide to Monitoring and Tuning the Linux Networking Stack: Receiving Data
NAPI

原文

oprofile

发表于 2020-06-17 分类于 operate system
本文字数: 47 阅读时长 ≈ 1 分钟

https://oprofile.sourceforge.io/doc/index.html

kernel module

发表于 2020-05-28 更新于 2020-05-29
本文字数: 165 阅读时长 ≈ 1 分钟
1
2
3
4
 char *ethname = NULL;
module_param(ethname, charp, S_IRUGO);
static bool checksum = true;
module_param(checksum, bool, S_IRUGO | S_IWUSR);

/sys/module目录下,可以找到内核模块

海盗分金

发表于 2020-04-20
本文字数: 5.1k 阅读时长 ≈ 5 分钟

海盗分金

经济学上有个“海盗分金”模型:是说5个海盗抢得100枚金币,他们按抽签的顺序依次提方案:首先由1号提出分配方案,然后5人表决,投票要超过半数同意方案才被通过,否则他将被扔入大海喂鲨鱼,依此类推。“海盗分金”其实是一个高度简化和抽象的模型,体现了博弈的思想。在“海盗分金”模型中,任何“分配者”想让自己的方案获得通过的关键是事先考虑清楚“挑战者”的分配方案是什么,并用最小的代价获取最大收益,拉拢“挑战者”分配方案中最不得意的人们。

模型

假设前提

假定“每个海盗都是绝顶聪明且很理智”,那么“第一个海盗提出怎样的分配方案才能够使自己的收益最大化?”

推理过程

推理过程是这样的:
从后向前推,如果1至3号强盗都喂了鲨鱼,只剩4号和5号的话,5号一定投反对票让4号喂鲨鱼,以独吞全部金币。所以,4号惟有支持3号才能保命。

3号知道这一点,就会提出“100,0,0”的分配方案,对4号、5号一毛不拔而将全部金币归为已有,因为他知道4号一无所获但还是会投赞成票,再加上自己一票,他的方案即可通过。

不过,2号推知3号的方案,就会提出“98,0,1,1”的方案,即放弃3号,而给予4号和5号各一枚金币。由于该方案对于4号和5号来说比在3号分配时更为有利,他们将支持他而不希望他出局而由3号来分配。这样,2号将拿走98枚金币。

同样,2号的方案也会被1号所洞悉,1号并将提出(97,0,1,2,0)或(97,0,1,0,2)的方案,即放弃2号,而给3号一枚金币,同时给4号(或5号)2枚金币。由于1号的这一方案对于3号和4号(或5号)来说,相比2号分配时更优,他们将投1号的赞成票,再加上1号自己的票,1号的方案可获通过,97枚金币可轻松落入囊中。这无疑是1号能够获取最大收益的方案了!答案是:1号强盗分给3号1枚金币,分给4号或5号强盗2枚,自己独得97枚。分配方案可写成(97,0,1,2,0)或(97,0,1,0,2)。

企业中的一把手,在搞内部人控制时,经常是抛开二号人物,而与会计和出纳们打得火热,就是因为公司里的小人物好收买。

1号看起来最有可能喂鲨鱼,但他牢牢地把握住先发优势,结果不但消除了死亡威胁,还收益最大。这不正是全球化过程中先进国家的先发优势吗?而5号,看起来最安全,没有死亡的威胁,甚至还能坐收渔人之利,却因不得不看别人脸色行事而只能分得一小杯羹。

不过,模型任意改变一个假设条件,最终结果都不一样。而现实世界远比模型复杂。

首先,现实中肯定不会是人人都“绝对理性”。回到“海盗分金”的模型中,只要3号、4号或5号中有一个人偏离了绝对聪明的假设,海盗1号无论怎么分都可能会被扔到海里去了。所以,1号首先要考虑的就是他的海盗兄弟们的聪明和理性究竟靠得住靠不住,否则先分者倒霉。
如果某人偏好看同伙被扔进海里喂鲨鱼。果真如此,1号自以为得意的方案岂不成了自掘坟墓!

再就是俗话所说的“人心隔肚皮”。由于信息不对称,谎言和虚假承诺就大有用武之地,而阴谋也会像杂草般疯长,并借机获益。如果2号对3、4、5号大放烟幕弹,宣称对于1号所提出任何分配方案,他一定会再多加上一个金币给他们。这样,结果又当如何?

通常,现实中人人都有自认的公平标准,因而时常会嘟嚷:“谁动了我的奶酪?”可以料想,一旦1号所提方案和其所想的不符,就会有人大闹……当大家都闹起来的时候,1号能拿着97枚金币毫发无损、镇定自若地走出去吗?最大的可能就是,海盗们会要求修改规则,然后重新分配。想一想二战前的希特勒德国吧!

而假如由一次博弈变成重复博弈呢?比如,大家讲清楚下次再得100枚金币时,先由2号海盗来分……然后是3号……

最可怕的是其他四人形成一个反1号的大联盟并制定出新规则:四人平分金币,将1号扔进大海…这就是穷人平均财富,将富人丢进海里的仇富机械平均理念。

制度规范行为,理性战胜愚昧!

如果假设变为,是10人分100枚金币,投票50%或以上才能通过,否则他将被扔入大海喂鲨鱼,依此类推。50%是问题的关键,海盗可以投自己的票。因此如果剩下两个人,无论什么方案都会被通过,即100,0。

往上推一步,3个人时,倒数第三个人知道只剩两个人时的分配情况,因此它会团结最后一个人,给他一个金币
“往前推一步。当前加一个更凶猛的海盗P8。P10知道———P8知道他知道———如果P8的方案被否决了,游戏就会只由P9和P10来继续,而P10就一枚金币也得不到。所以P8知道,只要给P10一枚金币,P10就会同意他的方案(当然,如果不给P10一枚金币,P10反正什么也得不到,宁可投票让P8去喂鱼)。所以P8的最佳策略是:P10得1枚,P9什么也得不到,P8得99枚。

P7的情况差不多。他只要得一票就可以了,给P9一枚金币就可以让他投票赞同这个方案,因为在接下来P8的方案中P9什么也得不到。P6也是相同的推理方法只不过他要说服他的两个同伴,于是他给在P7方案中什么也得不到的P8和P10一枚金币,自己留下98枚。

依此类推,最终P1的最佳方案是:他自己得96枚,给每一个在P2方案中什么也得不到的P3、P5、P7和P9一枚金币。

结果

结果,“海盗分金”最后的结果是P1、P2、P3、P4、P5、P6、P7、P8、P9、P10各可以获得96、0、1、0、1、0、1、0、1、0枚金币。
在“海盗分金”中,任何“分配者”想让自己的方案获得通过的关键是,事先考虑清楚“挑战者”的分配方案是什么,并用最小的代价获取最大收益,拉拢“挑战者”分配方案中最不得意的人们。

真地是难以置信。P1看起来最有可能喂鲨鱼,但他牢牢地把握住先发优势,结果不但消除了死亡威胁,还获得了最大收益。而P10,看起来最安全,没有死亡的威胁,甚至还能坐收渔人之利,但却因不得不看别人脸色行事,结果连一小杯羹都无法分到,却只能够保住性命而已。

演绎推理

假设

5个海盗抢到了100枚金币,每一颗都一样的大小和价值。

他们决定这么分:

  1. 抽签决定自己的号码(1,2,3,4,5)
  2. 首先,由1号提出分配方案,然后大家5人进行表决,当半数以上的人同意时(包括半数),按照他的提案进行分配,否则将被扔入大海喂鲨鱼。
  3. 如果1号死后,再由2号提出分配方案,然后大家4人进行表决,当且仅当半超过半数的人同意时,按照他的提案进行分配,否则将被扔入大海喂鲨鱼。
  4. 依次类推……

    条件

    每个海盗都是很聪明的人,都能很理智的判断得失,从而做出选择。

    问题

    第一个海盗提出怎样的分配方案才能够使自己的收益最大化?

(如果在规则中加上下面一条会更加完善:海盗在自己的收益最大化的前提下乐意看到其他海盗被扔入大海喂鲨鱼。不加也说的过去,因为其他海盗被扔入大海喂鲨鱼符合每个海盗的最大化利益。)

使用

首先到了4号提出的方案的时候肯定是最终方案,因为不管5号同意不同意都能通过,所以4号5号不必担心自己被投入大海。那此时5号获得的金币为0,4号获得的金币为100。

5号:因为4号提方案的时候 ,自己获取的金币为0 。所以只要4号之前的人分配给自己的金币大于0就同意该方案。

4号:如果3号提的方案一定能获得通过(原因:3号给5号的金币大于0, 5号就同意 因此就能通过),那自己获得的金币就为0,所以只要2号让自己获得的金币大于0就会同意。

3号:因为到了自己提方案的时候可以给5号一金币,自己的方案就能通过,但考虑到2号提方案的时候给4号一个金币,2号的方案就会通过,那自己获得的金币就为0。所以只要1号让自己获得的金币大于0就会同意。

2号:因为到了自己提方案的时候只要给4号一金币,就能获得通过,根本就不用顾及3 号 5号同意不同意,所以不管1号怎么提都不会同意。

1号:2号肯定不会同意。但只要给3号一块金币,5号一块金币(因为5号如果不同意,那么4号分配的时候,他什么都拿不到)就能获得通过。

所以答案是
98,0,1,0,1。

推理过程

推理①:

假设①:1、2、3号已被扔入海中,由4号分宝石。

由假设①推理出:

结论① :4号的方案必为100、0,且必定通过。(故4号不可能被扔入海中,与假设①不矛盾)

推理②:(要用到推理①的结论)

假设②:1、2号已被扔入海中,由3号分宝石。

由结论①、假设② 推理出:

结论②: 3号进行“推理①”的推理,得到结论①后,知道了:自己只需给5号多于0个宝石,即方案为99、0、1,其方案就必定通过。(故3号不可能被扔入海中,与假设②不矛盾,只要与假设②不矛盾就行了,与假设①没有丝毫关系,因为它们是两个互相独立的推理。)

余下的推理依次类推。

本题推广

有X(1=<X=<202)个海盗,100颗宝石,其它规则同上。

则1号海盗的最大化收益 Y =101-((X+1)/2所得数取整)。

(当X=201及X=202时,1号海盗的最大化收益为0,但可保命。)

Z(2=<Z=<X)号海盗的收益:Z为奇数时收益为 1, Z为偶数时收益为 0 。

对于X>202时情况,可先在X=500个的情况下进行讨论,然后再作推广。

依然是使用倒推法。

203号海盗必须获得102张赞成票,但他无法用100个宝石收买到101名同伙的支持。因此,无论203号提出什么样的分配方案,他都注定会被扔到海里去喂鱼。

204号海盗必须获得102张赞成票,203号为了能保住性命,就必须让204号的方案通过,避免由203号自己来提出分配方案,所以无论204号海盗提出什么样的方案,都可以得到203号的坚定支持。这样204号海盗就可以保命:他可以得到他自己的1票、203号的1票、以及用100个宝石收买到的100名同伙的赞成票,刚好达到所需的半数支持。能从204号那里获得1个宝石的海盗,必属于按照202号海盗的方案将一无所获的那102名海盗之列。

205号海盗必须获得103张赞成票,但他无法用100个宝石收买到102名同伙的支持。因此,无论205提出什么样的分配方案,他都注定会被扔到海里去喂鱼。

206号海盗必须获得103张赞成票,他可以得到205号的坚定支持,但他无法用100个宝石收买到101名同伙的支持。因此,无论206号提出什么样的分配方案,他都注定会被扔到海里去喂鱼。

207号海盗必须获得104张赞成票,他可以得到205号和206号的坚定支持,但他无法用100个宝石收买到101名同伙的支持。因此,无论207号提出什么样的分配方案,他都注定会被扔到海里去喂鱼。

208号海盗必须获得104张赞成票,他可以得到205号、206号、207号的坚定支持,加上他自己1票以及收买的100票,使他得以保命。从208号那里获得1个宝石的海盗,必属于那些按照204号方案将一无所获的那104名海盗之列。

眼下可以看出一条新的、此后将一直有效的规律:那些方案能通过的海盗(他们的分配方案全都是把宝石用来收买100名同伙,自己连1个宝石都得不到)相隔的距离越来越远,而在他们之间的海盗则无论提出什么样的方案都会被扔进海里。因此,为了保命,他们必会投票支持排在他们前面的海盗提出的任何分配方案。得以避免葬身鱼腹的海盗包括201、202、204、208、216、232、264、328、456号,

即200+1、200+2、200+4、200+8、200+16、200+32、200+64、200+128、200+256。

即
200+2的0次幂,200+2的1次幂,200+2的2次幂,200+2的3次幂,200+2的4次幂,200+2的5次幂,200+2的6次幂,200+2的7次幂,200+2的8次幂,

即其号码等于200加2的某次幂。

对本题作更一般的推广

有X个海盗,A 颗宝石,其它规则同上。

当X<2A+2时,

则1号海盗的最大化收益 Y=A+1-((X+1)/2所得数取整)。

(当X=2A+1时,1号海盗的最大化收益为0,但可保命。)

Z号(2=<Z=<X)海盗的收益:Z为奇数时收益为 1, Z为偶数时收益为 0 。

当X>=2A+2时,

若X=2A+2的B次幂,则1号海盗可保命,但无收益。其他海盗的收益情况由前面讨论可知有规律,但海盗的编号不固定,对它们的表述省略。

若X不等于2A+2的某次幂,设B=b是能使(X>2A+2的B次幂)成立的最大B,则(X+1-(2A+2的b次幂))号海盗可保命,但无收益。之前的海盗都会被扔到海里去喂鱼。之后的海盗的收益情况由前面讨论可知有规律,但海盗的编号不固定,对它们的表述省略。

netlink

发表于 2020-03-12 更新于 2020-04-07
本文字数: 59 阅读时长 ≈ 1 分钟

https://e-mailky.github.io/2017-02-14-netlink-user-kernel1

1…678…26
AIRobot

AIRobot

AIRobot quick note
130 日志
15 分类
23 标签
GitHub E-Mail
Creative Commons
0%
© 2023 AIRobot | 716k | 10:51