About neohope

一直在努力,还没想过要放弃...

一次技术中台升级记录

由于历史原因,我们有两套技术架构:
基于SpringBoot1.X的老框架、老技术中台、老数据中台。
基于SpringBoot2.X的新框架、新技术中台、新数据中台。

老技术中台研发的时候,整个生产环境都是我们自己搭建的,很多功能需要自研。
到了新技术中台时,已经迁移到了集团的云上,之前设计的很多功能也就用不到了。
而且,随着时间的推移,老中台人员都去参与了其他项目,长期处于支持维护状态,无论研发、运维还是安全同事,都感到压力山大。
由于云端环境时不时会做调整,运维同学都不敢轻易重启老中台服务,怕一重启,就再也起不来了。
安全扫描策略也不断升级,扫描出来的问题一次比一次多,很快就千疮百孔了,但大家也只能缝缝补补。

之前由于业务压力异常巨大,大家一直在忙着做各种新的业务功能,虽然一直想升级,但一直没有狠下心来。
到了今年四季度,遇到了几次生产事故后,大家痛定思痛,决定还是干吧,把老中台下掉。

整体步骤大体如下:
1、找了两位对老技术中台比较熟悉的同事,对全部服务进行了梳理
2、各技术团队,也对老中台的依赖关系重新进行了梳理
3、大家坐下来,对服务进行了分类
A、对于网关类服务,按领域拆分
B、对于调用中转类服务,已经不符合当前要求,改为K8S的微服务直连
C、先看调用量小的服务;没有调用量的服务及接口,尽早下线
D、其余调用量很少的服务,与业务方沟通,能下线下线,不能下线合并等待下线
E、新老技术中台都有的服务,新中台进行适配,下线老中台服务
F、老中台有,新中台没有的中台类服务,功能迁移,下线老中台服务
G、非中台类服务,拆分到业务系统,下线老中台服务
H、其余服务,一事一议
4、对新老平台都保持的数据,进行数据治理,统一数据标准
5、在服务迁移改造的过程中,要求按新框架规范,将服务升级到SpringBoot2
6、由于没有业务上的直接收益,所以整个改造周期排了很久,各团队根据实际情况,在每次迭代中把工作量消化掉
7、及时与测试同学沟通,确保每次测试覆盖到
8、与运维同学沟通,服务全部升级完成后,切换nacos到最新版本(之前受限于SpringBoot1,无法升级nacos)
9、这样框架、技术中台就升级完毕了,后面每半年升级一次框架就可以了
10、数据中台的小伙伴,因形势所迫,直接抛弃了原有技术栈和工具,拥抱了新技术栈,最后技术债务居然是最小的。

任务已排期,各团队都表示坚决支持。但你以为这样就能顺利推进吗?我感觉比较难哦。
过半年,再续写一下,推进情况究竟如何,敬请期待。

记录一个卡券系统漏洞

大概三年前,遇到过这样一个系统需求,被安全团队及时发现,避免了引起大规模的问题。

事情是这样的,我们有一个比较老的外购卡券管理系统,之前都是实体卡直接核销的。
后来逐步上线了各大电商平台,就需要通过虚拟卡的方式,通过短信将激活地址、卡号、卡密直接发送给客户,客户可以直接注册、激活。

但这时遇到了两个问题:
1、客户需要复制卡号、卡密到激活地址,操作比较繁琐
2、卡号、卡密都很长,会造成一个短信无法成功发送,需要拆分成两个短信发送,同样是不方便

业务方希望客户操作更加方便,与供应商沟通,供应商提供了这样一个方案:
1、客户下单完成后,将激活地址+卡号+卡密,一起生成一个短链
2、直接将短链通过短信发给客户
3、客户点击短链,一键绑定

听起来一起都很美好是不是?

但很快就被安全团队质疑了,攻击思路是这样的:
1、暴力枚举短链,批量获取短链实际地址
2、匹配地址,批量抓取卡号卡密
3、申请账号,绑定卡号卡密,攻击成功

于是这个需求被判定为高危风险,直接就被否定了。

Icarus Verilog环境搭建

1、下载Icarus Verilog
Icarus Verilog:
http://iverilog.icarus.com/home

Icarus Verilog for Windows:
http://bleyer.org/icarus/

vscode插件
Verilog-HDL/SystemVerilog/Bluespec SystemVerilog

2、下载示例源码
极客时间 操作系统实战45讲 Icarus Verilog示例源码

3、编译

//iverilog路径,要加入到环境变量中,让vscode可以访问
iverilog -o my_alu alu_test.v alu.v
vvp my_alu
gtkwave -F my_alu.vcd

4、在gtkwave中能查看波形咯
IcarusVerilog.png

Linux虚拟化管理

1、内核模块初始化

module_init(vmx_init)->kvm_init
module_init(svm_init)->kvm_init

//其中,kvm_init
->kvm_arch_init
->kvm_irqfd_init
->kvm_arch_hardware_setup
->misc_register(&kvm_dev)

2、从数据结构角度,又可以看到了设备皆为文件的思想

static struct miscdevice kvm_dev = {
    KVM_MINOR,
    "kvm",
    &kvm_chardev_ops,
};

static struct file_operations kvm_chardev_ops = {
    .unlocked_ioctl = kvm_dev_ioctl,
    .llseek     = noop_llseek,
    KVM_COMPAT(kvm_dev_ioctl),
};

//初始化时,通过misc_register,实现了操作的绑定。

3、通过上面的数据结构,我们就可以找到创建虚拟机的方法,并生成控制文件
kvm_dev.kvm_chardev_ops.kvm_dev_ioctl
或者,ioctl系统调用KVM_CREATE_VM,效果也是一样的:

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
->vfs_ioctl,会用到vfs_ioctl.unlocked_ioctl也就是kvm_dev_ioctl

->case KVM_CREATE_VM:
->        r = kvm_dev_ioctl_create_vm(arg);
->file = anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR);

//其中,kvm_dev_ioctl_create_vm
->kvm_create_vm
->->kvm_arch_init_vm
->->hardware_enable_all
->->kvm_arch_post_init_vm
->->list_add(&kvm->vm_list, &vm_list);

4、生成虚拟CPU套路很相似,仍是文件操作

static struct file_operations kvm_vm_fops = {
    .release        = kvm_vm_release,
    .unlocked_ioctl = kvm_vm_ioctl,
    .llseek     = noop_llseek,
    KVM_COMPAT(kvm_vm_compat_ioctl),
};

//创建虚拟机时,通过anon_inode_getfile,实际上就把文件和kvm_vm_fops绑定了起来。
anon_inode_getfile("kvm-vm", &kvm_vm_fops, kvm, O_RDWR)

5、在调用ioctl时

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
->vfs_ioctl,会用到vfs_ioctl.unlocked_ioctl也就是kvm_vm_ioctl

kvm_vm_ioctl->kvm_vm_ioctl_create_vcpu
->kvm_arch_vcpu_precreate
->kvm_vcpu_init
->kvm_arch_vcpu_create
->kvm_get_kvm
->create_vcpu_fd,生成设备文件inode
->kvm_arch_vcpu_postcreate

//其中,kvm_arch_vcpu_create
->kvm_mmu_create
->vcpu->arch.user_fpu = kmem_cache_zalloc(x86_fpu_cache, GFP_KERNEL_ACCOUNT);
->kvm_pmu_init(vcpu);
->kvm_hv_vcpu_init(vcpu);
->kvm_x86_ops.vcpu_create(vcpu);
->kvm_vcpu_mtrr_init(vcpu);
->vcpu_load(vcpu);
->kvm_vcpu_reset(vcpu, false);
->kvm_init_mmu(vcpu, false);  //包括init_kvm_tdp_mmu和init_kvm_softmmu两种虚拟化方式

6、启动虚拟机,还是文件操作

static struct file_operations kvm_vcpu_fops = {
    .release        = kvm_vcpu_release,
    .unlocked_ioctl = kvm_vcpu_ioctl,
    .mmap           = kvm_vcpu_mmap,
    .llseek     = noop_llseek,
    KVM_COMPAT(kvm_vcpu_compat_ioctl),
};

7、在调用ioctl时KVM_RUN

SYSCALL_DEFINE3(ioctl, unsigned int, fd, unsigned int, cmd, unsigned long, arg)
->vfs_ioctl,会用到vfs_ioctl.unlocked_ioctl也就是kvm_vcpu_ioctl

kvm_vcpu_ioctl->
case KVM_RUN: 
  kvm_arch_vcpu_ioctl_run

//其中,
kvm_arch_vcpu_ioctl_run->vcpu_run->vcpu_enter_guest

8、IO同样有虚拟化和半虚拟化两种
一个处理函数为kvm_fast_pio,另一个为kvm_emulate_instruction

Linux系统调用03

Linux系统调用的整体流程为:
1、应用程序【用户态】通过syscall或glibc进行内核功能调用,这一部分在glibc源码中进行的
2、CPU收到syscall,Linux内核响应syscall调用【内核态】,这一部分在linux源码中进行的
3、返回结果到应用程序【用户态】

本节,给Linux系统,增加一个新系统调用功能,获取cpu数量。

1、新建一个源码编译目录

mkdir kernelbuild

2、下载源码,解压

wget https://mirrors.edge.kernel.org/pub/linux/kernel/v5.x/linux-5.10.59.tar.gz   

tar -xzf linux-5.10.59.tar.gz   

cd linux-5.10.59

3、清理

make mrproper

4、修改文件

4.1、arch/x86/entry/syscalls/syscall_64.tbl
#在440后面增加一行
441  common  get_cpus    sys_get_cpus

4.2、include/linux/syscalls.h
#在最后一个asmlinkage增加一行
asmlinkage long sys_get_cpus(void);

4.3、kernel/sys.c  
#在最后一个SYSCALL_DEFINE0后面增加下面几行
//获取系统中有多少CPU
SYSCALL_DEFINE0(get_cpus)
{
    return num_present_cpus();
}

5、内核配置

make menuconfig
make oldconfig

6、修改.config,去掉一个证书

CONFIG_SYSTEM_TRUSTED_KEYS=“”

7、编译

make -j4

8、安装

sudo make modules_install
sudo make install

9、测试
9.1、新建文件cpus.c

#include <stdio.h>
#include <unistd.h>
#include <sys/syscall.h>
int main(int argc, char const *argv[])
{
    //syscall就是根据系统调用号调用相应的系统调用
    long cpus = syscall(441);
    printf("cpu num is:%d\n", cpus);//输出结果
    return 0;
}

9.2、编译

gcc main.c -o cpus

9.3、运行

./cpus
在没有修改的内核上返回是-1
在修改过的为num_present_cpus数量

极客时间 操作系统实战45讲 Linux系统调用源码

Linux系统调用02

Linux系统调用的整体流程为:
1、应用程序【用户态】通过syscall或glibc进行内核功能调用,这一部分在glibc源码中进行的
2、CPU收到syscall,Linux内核响应syscall调用【内核态】,这一部分在linux源码中进行的
3、返回结果到应用程序【用户态】
本节处理第二部分:

二、linux内核部分

1、在make时,会通过syscall_64.tbl生成syscalls_64.h,然后包含到syscall_64.c,进行调用号与函数之间的绑定。
arch/x86/entry/syscalls/syscall_64.tbl
arch/x86/include/generated/asm/syscalls_64.h
arch/x86/entry/syscall_64.c

1.1、以sys_openat为例,在syscall_64.tbl中为

257    common    openat            sys_openat
441    common    get_cpus          sys_get_cpus

1.2、make后,在生成的syscalls_64.h中为

__SYSCALL_COMMON(257, sys_openat)

1.3 在syscall_64.c中,展开__SYSCALL_COMMON

#define __SYSCALL_COMMON(nr, sym) __SYSCALL_64(nr, sym)
//展开就是
__SYSCALL_64(257, sys_openat)

1.4、在syscall_64.c中,第一次展开__SYSCALL_64

#define __SYSCALL_64(nr, sym) extern long __x64_##sym(const struct pt_regs *);
#include <asm/syscalls_64.h>
#undef __SYSCALL_64

//展开就是
extern long __x64_sys_openat(const struct pt_regs *);

//也就是每个__SYSCALL_64都展开成了一个外部函数

1.5、在syscall_64.c中,第二次展开__SYSCALL_64

#define __SYSCALL_64(nr, sym) [nr] = __x64_##sym,

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    [0 ... __NR_syscall_max] = &__x64_sys_ni_syscall,
#include <asm/syscalls_64.h>
};

//展开其实就是指向了外部函数
[257]=__x64_sys_openat,

//全部展开结果,都会被包含到sys_call_table中,从而完成了调用号与函数之间的绑定。

2、当产生系统调用时
2.1、应用直接syscall或通过glibc产生了syscall

2.2、cpu会产生类似于中断的效果,开始到entry_SYSCALL_64执行

//文件路径arch/x86/entry/entry_64.S
SYM_CODE_START(entry_SYSCALL_64)
//省略代码
call   do_syscall_64
SYM_CODE_END(entry_SYSCALL_64)

//文件路径arch/x86/entry/entry_64.S,32位兼容模式,过程与64位类似
SYM_CODE_START(entry_SYSCALL_compat)
call   do_fast_syscall_32
SYM_CODE_END(entry_SYSCALL_compat)

2.3、调用do_syscall_64

#ifdef CONFIG_X86_64
__visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
    nr = syscall_enter_from_user_mode(regs, nr);
    instrumentation_begin();
    if (likely(nr < NR_syscalls)) {
        nr = array_index_nospec(nr, NR_syscalls);
        regs->ax = sys_call_table[nr](regs);
    }
    instrumentation_end();
    syscall_exit_to_user_mode(regs);
}
#endif

2.4、根据sys_call_table调用对应的功能函数

sys_call_table[nr](regs)
如果我们传入257,就会调用__x64_sys_openat
如果我们传入441,就会调用__x64_sys_get_cpus

2.5、但咱们实际写的函数sys_get_cpus,好像和实际调用函数__x64_sys_get_cpus,差了一个__x64,这需要一个wrapper

arch\x86\include\asm\syscall_wrapper.h
#define SYSCALL_DEFINE0(sname)                      \
    SYSCALL_METADATA(_##sname, 0);                  \
    static long __do_sys_##sname(const struct pt_regs *__unused);   \
    __X64_SYS_STUB0(sname)                      \
    __IA32_SYS_STUB0(sname)                     \
    static long __do_sys_##sname(const struct pt_regs *__unused)

#define __X64_SYS_STUB0(name)                       \
    __SYS_STUB0(x64, sys_##name)

#define __SYS_STUB0(abi, name)                      \
    long __##abi##_##name(const struct pt_regs *regs);      \
    ALLOW_ERROR_INJECTION(__##abi##_##name, ERRNO);         \
    long __##abi##_##name(const struct pt_regs *regs)       \
        __alias(__do_##name);

SYSCALL_DEFINE0(get_cpus),会展开成为
__X64_SYS_STUB0(get_cpus) 
//然后
 __SYS_STUB0(x64, sys_get_cpus)
//然后
long __x64_sys_get_cpus(const struct pt_regs *regs);

这样前后就对上了,glibc和linux内核就通了。

Linux系统调用01

Linux系统调用的整体流程为:
1、应用程序【用户态】通过syscall或glibc进行内核功能调用,这一部分在glibc源码中进行的
2、CPU收到syscall,Linux内核响应syscall调用【内核态】,这一部分在linux源码中进行的
3、返回结果到应用程序【用户态】
本节先处理第一部分:

一、glibc部分
1、应用程序调用open函数

//glibc/intl/loadmsgcat.c
# define open(name, flags)  __open_nocancel (name, flags)

2、展开后实际上调用了

__open_nocancel(name, flags)

3、而__open_nocancel 最终调用了INLINE_SYSCALL_CALL

//glibc/sysdeps/unix/sysv/linux/open_nocancel.c
__open_nocancel(name, flags)
->return INLINE_SYSCALL_CALL (openat, AT_FDCWD, file, oflag, mode);

4、宏展开【理解就好,不保证顺序】

4.1、初始为

INLINE_SYSCALL_CALL (openat, AT_FDCWD, file, oflag, mode);

4.2、第1次展开INLINE_SYSCALL_CALL
[code lang="c"]
#define INLINE_SYSCALL_CALL(...) \
  __INLINE_SYSCALL_DISP (__INLINE_SYSCALL, __VA_ARGS__)

//展开得到:
__INLINE_SYSCALL_DISP(__INLINE_SYSCALL, __VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】)

4.3、第2次展开__INLINE_SYSCALL_DISP
[code lang="c"]
#define __INLINE_SYSCALL_DISP(b,...) \
  __SYSCALL_CONCAT (b,__INLINE_SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__)

//展开得到:
__SYSCALL_CONCAT(b【__INLINE_SYSCALL】,__INLINE_SYSCALL_NARGS(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】))(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】)

4.4、第3次展开__INLINE_SYSCALL_NARGS

__INLINE_SYSCALL_NARGS(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】)

#define __INLINE_SYSCALL_NARGS(...) \
  __INLINE_SYSCALL_NARGS_X (__VA_ARGS__,7,6,5,4,3,2,1,0,)

//展开得到:
__INLINE_SYSCALL_NARGS_X(openat, AT_FDCWD, file, oflag, mode,7,6,5,4,3,2,1,0,)

//然后展开__INLINE_SYSCALL_NARGS_X
#define __INLINE_SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n

//展开得到参数个数:
4

//从而4.4的结果为
__SYSCALL_CONCAT(__INLINE_SYSCALL,4)(__VA_ARGS__【openat, AT_FDCWD, file, oflag, mode】)

4.5、然后展开__SYSCALL_CONCAT,其实就是字符拼接

__SYSCALL_CONCAT(__INLINE_SYSCALL,4)

#define __SYSCALL_CONCAT_X(a,b)     a##b
#define __SYSCALL_CONCAT(a,b)       __SYSCALL_CONCAT_X (a, b)

//展开得到:
__INLINE_SYSCALL4

//从而4.5的结果为
__INLINE_SYSCALL4(openat, AT_FDCWD, file, oflag, mode)

4.6、然后展开INTERNAL_SYSCALL4

#define __INLINE_SYSCALL4(name, a1, a2, a3, a4) \
  INLINE_SYSCALL (name, 4, a1, a2, a3, a4)

//展开得到:
INLINE_SYSCALL(openat, 4, AT_FDCWD, file, oflag, mode)

4.7、展开INLINE_SYSCALL

//glibc/sysdeps/unix/sysv/linux/sysdep.h
#define INLINE_SYSCALL(name, nr, args...)       \
  ({                  \
    long int sc_ret = INTERNAL_SYSCALL (name, nr, args);    \
    __glibc_unlikely (INTERNAL_SYSCALL_ERROR_P (sc_ret))    \
    ? SYSCALL_ERROR_LABEL (INTERNAL_SYSCALL_ERRNO (sc_ret))   \
    : sc_ret;               \
  })

//展开得到
INTERNAL_SYSCALL (openat, 4, args【AT_FDCWD, file, oflag, mode】);  

4.8、展开INTERNAL_SYSCALL

#define INTERNAL_SYSCALL(name, nr, args...)       \
  internal_syscall##nr (SYS_ify (name), args)

//展开得到
internal_syscall4(SYS_ify(openat), args【AT_FDCWD, file, oflag, mode】)

//展开 SYS_ify(openat)
#define SYS_ify(syscall_name) __NR_##syscall_name
//得到
__NR_openat

//从而得到
internal_syscall4(__NR_openat, args【AT_FDCWD, file, oflag, mode】)

4.9、最后internal_syscall4中,汇编调用了syscall

glibc\sysdeps\unix\sysv\linux\x86_64\64\arch-syscall.h
#define __NR_openat 257

最终,syscall时,先传入调用号257,然后是四个真正的参数。

Mininet+ONOS网络模拟环境搭建

1、下载mininet虚拟机:

http://mininet.org/download/#option-1-mininet-vm-installation-easy-recommended

2、导入并运行虚拟机

用户名/密码:mininet/mininet

3、安装docker

sudo apt-get update
sudo apt install curl ssh
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun

4、拉取onos镜像

sudo docker pull onosproject/onos

5、运行ones容器

#运行
sudo docker run -t -d -p 8181:8181 --name onos1 onosproject/onos

#查看容器运行情况
sudo docker ps

6、连接容器,启用服务

#查看ip地址
sudo docker inspect --format '{{ .NetworkSettings.IPAddress }}' onos1

#ssh连接到容器
ssh -p 8101 karaf@172.17.0.2

#启用OpenFlow Provider Suite
app activate org.onosproject.openflow

#启用 Reactive Forwarding
app activate org.onosproject.fwd

#退出
ctrl+d

7、创建网络

#创建临时网络
sudo mn --topo tree,2 --controller remote,ip=172.17.0.2 --switch=ovsk,protocols=OpenFlow13
*** Creating network
*** Adding controller
Connecting to remote controller at 172.17.0.2:6653
*** Adding hosts:
h1 h2 h3 h4
*** Adding switches:
s1 s2 s3
*** Adding links:
(s1, s2) (s1, s3) (s2, h1) (s2, h2) (s3, h3) (s3, h4)
*** Configuring hosts
h1 h2 h3 h4
*** Starting controller
c0
*** Starting 3 switches
s1 s2 s3 ...
*** Starting CLI:

mininet> nodes
available nodes are:
c0 h1 h2 h3 h4 s1 s2 s3

mininet> links
s1-eth1<->s2-eth3 (OK OK)
s1-eth2<->s3-eth3 (OK OK)
s2-eth1<->h1-eth0 (OK OK)
s2-eth2<->h2-eth0 (OK OK)
s3-eth1<->h3-eth0 (OK OK)
s3-eth2<->h4-eth0 (OK OK)

mininet> net
h1 h1-eth0:s2-eth1
h2 h2-eth0:s2-eth2
h3 h3-eth0:s3-eth1
h4 h4-eth0:s3-eth2
s1 lo: s1-eth1:s2-eth3 s1-eth2:s3-eth3
s2 lo: s2-eth1:h1-eth0 s2-eth2:h2-eth0 s2-eth3:s1-eth1
s3 lo: s3-eth1:h3-eth0 s3-eth2:h4-eth0 s3-eth3:s1-eth2
c0

mininet> h1 ping h2
PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=43.2 ms

mininet> pingall
*** Ping: testing ping reachability
h1 -> h2 h3 h4
h2 -> h1 h3 h4
h3 -> h1 h2 h4
h4 -> h1 h2 h3
*** Results: 0% dropped (12/12 received)

#退出
ctrl+d

8、网络访问

http://172.17.0.2:8181/onos/ui/login.html
账号/密码:karaf/karaf

然后就可以看到拓扑图了

Linux内存管理

一、整理一下思路
NUMA体系下,每个CPU都有自己直接管理的一部分内存,叫做内存节点【node】,CPU访问自己的内存节点速度,快于访问其他CPU的内存节点;
每个内存节点,按内存的迁移类型,被划分为多个内存区域【zone】;迁移类型包括ZONE_DMA、ZONE_DMA32、ZONE_NORMAL 、ZONE_HIGHMEM、ZONE_MOVABLE、ZONE_DEVICE等;
每个内存区域中,是一段逻辑上的连续内存,包括多个可用页面;但在这个连续内存中,同样有不能使用的地方,叫做内存空洞;在处理内存操作时,要避免掉到洞里;

二、整理一下结构
每个内存节点由一个pg_data_t结构来描述其内存布局;
每个pg_data_t有一个zone数组,包括了内存节点下的全部内存区域zone;
每个zone里有一个free_area数组【0-10】,其序号n的元素下面,挂载了全部的连续2^n页面【page】,也就是free_area【0-10】分别挂载了【1个页面,2个页面,直到1024个页面】
每个free_area,都有一个链表数组,按不同迁移类型,对所属页面【page】再次进行了划分

三、分配内存【只能按2^n页面申请】

alloc_pages->alloc_pages_current->__alloc_pages_nodemask
->get_page_from_freelist,快速分配路径,尝试直接分配内存
->__alloc_pages_slowpath,慢速分配路径,尝试回收、压缩后,再分配内存,如果有OOM风险则杀死进程->实际分配时仍会调用get_page_from_freelist
->->所以无论快慢路径,都会到rmqueue
->->->如果申请一个页面rmqueue_pcplist->__rmqueue_pcplist
1、如果pcplist不为空,则返回一个页面
2、如果pcplist为空,则申请一块内存后,再返回一个页面
->->->如果申请多个页面__rmqueue_smallest
1、首先要取得 current_order【指定页面长度】 对应的 free_area 区中 page
2、若没有,就继续增加 current_order【去找一个更大的页面】,直到最大的 MAX_ORDER
3、要是得到一组连续 page 的首地址,就对其脱链,然后调用expand函数对内存进行分割
->->->->expand 函数分割内存
1、expand分割内存时,也是从大到小的顺序去分割的
2、每一次都对半分割,挂载到对应的free_area,也就加入了伙伴系统
3、直到得到所需大小的页面,就是我们申请到的页面了

四、此外
1、在整个过程中,有一个水位_watermark的概念,其实就是用于控制内存区是否需要进行内存回收
2、申请内存时,会先按请求的 migratetype 从对应类型的page结构块中寻找,如果不成功,才会从其他 migratetype 的 page 结构块中分配, 降低内存碎片【rmqueue->__rmqueue->__rmqueue_fallback】
3、申请内存时,一般先在CPU所属内存节点申请;如果失败,再去其他内存节点申请;具体顺序,和NUMA memory policy有关;