记一次存储Inode数量引发的生产故障

前一段时间,突然收到了系统报警,某上传服务异常。

经过排查,上传服务正常,但存储无法正常写入,一直写入失败,表现为:
1、一块新盘,32T,已使用2T,可用30T,控制台和命令行操作结果一直
2、服务写入时,一直报“no space left on device”
3、没有收到任何存储报警

立刻找了云服务厂商的老师,解决了问题:
1、除了限制写入文件总量的大小、并发写入的速度,同时还限制了inode数量
2、上传服务,写入了大量小文件,耗尽了inode数量
3、上传服务,再次写入后,inode申请失败,导致写入失败
4、存储组的老师,紧急扩展了inode数量,解决了问题

经排查,云服务商反馈:
1、为了控制成本,我们之前买了一块较小的硬盘,然后进行了扩容
2、而存储的底层协议为FlexGroup
3、而FlexGroup的普通卷,在扩容的时候,只要超过了1T,默认的Inode数量就一直为21251126,不再提升
4、而我们的上传服务,一个小文件只有几百k,很快就把Inode数量耗尽了
5、对于Inode数量限制,云服务商没有提供任何监控

虽然FlexGroup的超大卷默认会提升Inode数量,但我们一开始购买的服务确是普通卷,然后进行扩容,扩容后仍是普通卷,就触发了Inode数量不会自动增加这个问题。

后续,我们做了两个约定:
1、尽量采购超大卷
2、如果要采购普通卷,同时提单,增加Inode数量
3、云服务商同步进行产品更新,后续产品迭代时,从根源上解决这个问题

PS:
最近发现,他们居然做了一个inode扩容的功能,默认是最小值,可以手工扩展,也能设置为自动扩展。
不知道是谁定的需求,默认选项不应该是自动扩展吗?

导致惨重代价的运维事故02

导致惨重代价的运维事故02

OVH数据中心失火
2021年3月10日,欧洲云计算巨头OVH位于法国斯特拉斯堡的机房近日发生严重火灾,该区域总共有4个数据中心。
火灾持续6个小时才被扑灭,发生起火的SBG2数据中心被完全烧毁,共360万个网站下线。

Salesforce全球大宕机
2021年5月11日,Salesforce的服务宕机,持续5个小时。
事故原因,官方反馈为“工程师尝试通过脚本变更DNS配置,但脚本执行超时失败。但不幸的是,这个脚本一直在各节点扩散,最终导致全线崩溃”

访问量激增,导致各类一码通服务宕机
2021年12月20日,西安“一码通”崩溃。
2022年1月4日,西安“一码通”崩溃。
2022年1月10日,广州“粤康码”崩溃。
2022年3月11日,上海“随申码”崩溃。

Facebook严重宕机
2021年10月5日,Facebook旗下应用出现大面积故障,波及到Facebook、Messenger、Instagram和WhatsApp等几乎所有产品。
此次宕机长达7个小时,影响到全球数十个国家和地区的几十亿用户。
官方给出的事故原因为:“协调数据中心之间网络流量的主干路由器的配置变化导致了通信中断,由此对我们数据中心的通信方式产生了连带影响,使我们的服务陷入停顿”

小插曲:
Facebook股价盘中暴跌6%,市值减少数百亿美元,扎克伯格个人财富一日蒸发逾60亿美元。
宕机期间,大量用户涌向了Twitter、Telegram等其他应用,又进一步导致这些应用程序的服务器崩溃。

B站713事故
2022年7月13日,B站崩了5个小时。
根据B站的事故分析报告,是SLB故障导致
本次SLB故障,是OpenResty中,计算gcd的lua代码传入了0值,被lua判定为nan,陷入了死循环
这段lua代码已经稳定运行了一段事件,但一个新发布模式,却触发了这个bug

Twitter事故
2022年7月14日,Twitter崩了1个小时。

小插曲:
其实对于Twitter的崩溃,大家都已经习惯了,每年不崩溃几次,都感觉不正常。

阿里云香港机房重大事故
2022年12月18日,由于香港可用区C机房冷却系统失效,现场包间温度逐渐升高,导致一机房包间温度达到临界值触发消防系统喷淋,电源柜和多列机柜进水,部分机器硬件损坏,整个处置过程超过15小时。
事故后,阿里云总裁、CTO都被更换。

唯品会机房宕机事故
2023年3月29日凌晨,唯品会南沙IDC冷冻系统故障,导致机房设备温度快速升高宕机,造成线上商城停止服务
宕机事件达到12小时,损失超亿元,影响客户达800万,唯品会将此次故障判定为P0级故障。
事故后,基础平台部门负责人被免职。

腾讯云机房事故
23年3月29日凌晨,腾讯云广州五区部分云服务异常,导致微信、QQ、支付等核心功能受到影响,故障在当天中午基本恢复。
事故原因,官方反馈为“本次事故由广州电信机房冷却系统故障导致”。

微软Azure故障
2023年5月24日,微软Azure DevOps在巴西的一处scale-unit发生故障,导致宕机约10.5个小时。
导致该中断的原因为一个简单的拼写错误,最终导致17个生产级数据库被删除。

语雀重大服务故障
2023年10月23日,语雀出现重大服务故障,持续7个多小时才完全恢复。
故障原因为新的运维升级工具bug,导致华东地区生产环境存储服务器被误下线,造成大面积服务中断。

阿里云全球故障
2023年11月12日,阿里云爆发全球故障,阿里产品全线崩溃,几乎影响全部云用户,持续事件达3.5小时。
故障原因,具说是鉴权服务出了问题。

小插曲:
两周后,在2023年11月27日,阿里云再次遭遇了近两小时的中断,影响到中国和美国的客户。
然后当天晚上,滴滴就来了个大的。

滴滴故障
2023年11月27日晚间,滴滴崩溃,APP功能无法使用,直至第二天才陆续恢复,故障事件超过12小时。
故障原因,官方反馈说是“底层系统软件发生故障”。

腾讯云崩溃事件
24年4月8日,由于API服务新版本向前兼容性考虑不够和配置数据灰度机制不足,导致1957个客户报障,故障持续90分钟

微软蓝屏事件
24年7月19日,使用了Windows操作系统的设备大面积蓝屏,导致850万设备受到影响。
故障原因是,微软安全供应商CrowdStrike推送了错误的软件配置。

阿里云新加坡机房火灾
24年9月10日上午,阿里云新加坡可用区C数据中心发生火灾,导致主要科技公司服务中断。
火灾原因已确定为锂电池爆炸,持续36小时以上。
此故障,影响到了TikTok、Lazada等多家用户。

============================================================
注:本文主要是整理了运维导致的惨痛代价,并没有记录下面几种情况(设计失败,黑客攻击,病毒爆发)

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有关;

Linux进程管理

一、进程数据结构
每个CPU有一个rq结构,描述进程运行队列,其中:
A、cfs_rq、rt_rq、dl_rq,分别包含了公平调度、实时调度、最早截至时间调度算法相关的队列
B、记录了当前CPU的,正在运行的进程、空转进程、停止进程等;
C、每个进程用一个task_struct结构描述;

task_struct结构包括:
sched_entity结构,描述调度实体;
files_struct 结构,描述进程打开的文件;
mm_struct结构,描述一个进程的地址空间的数据结构;其中包括,vm_area_struct 结构,描述一段虚拟地址空间

二、fork创建一个进程

调用fork
->_do_fork
->->_do_fork首先调用复制进程copy_process
->->->调用了一系列的copy和初始化函数:dup_task_struct、copy_creds、copy_semundo、copy_files、copy_fs、copy_sighand、copy_signal、copy_mm、copy_namespaces、copy_io、copy_thread、copy_seccomp
->->_do_fork然后调用wake_up_new_task,初始化并准备好第一次启动,进入runqueue

其中,_do_fork->copy_process->dup_task_struct
A、alloc_task_struct_node,分配结构体
alloc_task_struct_node->kmem_cache_alloc_node->kmem_cache_alloc->slab_alloc->接上了之前的内容
B、alloc_thread_stack_node,分配内核栈
alloc_thread_stack_node->alloc_pages_node->__alloc_pages_node->__alloc_pages->__alloc_pages_nodemask->接上了之前的内容
C、arch_dup_task_struct复制task_struct
D、setup_thread_stack设置内核栈

其中,_do_fork->copy_process->copy_mm->dup_mm
A、allocate_mm,分配内存
B、memcpy,结构拷贝
C、mm_init,mm初始化
D、dup_mmap,mmap拷贝

其中,_do_fork->copy_files->dup_fd
kmem_cache_alloc,分配内存
copy_fd_bitmaps,拷贝fd位图数据

三、调度器数据结构
sched_class结构,通过一组函数指针描述了调度器;
__end_sched_classes,优先级最高
stop_sched_class,停止调度类
dl_sched_class,最早截至时间调度类
rt_sched_class,实时调度类
fair_sched_class,公平调度调度类
idle_sched_class,空转调度类
__begin_sched_classes,优先级最低

调度器的优先级,是编译时指定的,通过__begin_sched_classes和__end_sched_classes进行定位;

四、CFS调度
cfs调度算法,调度队列为cfs_rq,其整体是一个红黑树,树根记录在tasks_timeline中;
cfs调度器,根据一个进程权重占总体权重的比例,确定每个进程的CPU时间分配比例;而这个权重,开放给程序员的是一个nice值,数值越小,权重越大;

同时,即不能让进程切换过于频繁,也不能让进程长期饥饿,需要保证调度时间:
当进程数小于8个时,进程调度延迟为6ms,也就是每6ms保证每个进程至少运行一次;
当进程数大于8个时,进程延迟无法保证,需要确保程序至少运行一段时间才被调度,这个时间称为最小调度粒度时间,默认为0.75ms;

cfs中,由于每个进程的权重不同,所以无法单纯的通过进程运行时间来对进程优先级进行排序。所以将进程运行时间,通过权重换算,得到了一个进程运行的虚拟时间,然后通过虚拟时间,来对进程优先级进行排序。此时,红黑树的排序特性就充分发挥了,哪个进程的虚拟时间最小,就会来到红黑树的最左子节点,进行调度时,从左到右进行判断就好了。

这个时间又是如何刷新呢:

Linux会有一个scheduler_tick定时器,给调度器提供机会,刷新CFS队列虚拟时间
scheduler_tick->rq.curr.sched_class.task_tick,对应到CFS调度器,就是task_tick_fair
task_tick_fair->entity_tick
->update_curr,更新当前进程调度时间
->check_preempt_tick,根据实际运行时间、最小调度时间、虚拟时间是否最小等,判断是否要进行调度,如果需要调度则打标记

Linux进行进程调度时,调用schedule->__schedule
->pick_next_task
A、首先尝试pick_next_task_fair,获取下一个进程
B、如果获取失败,就按调取器优先级,依次尝试获取下一个进程
C、如果全部获取失败,就返回idel进程
->context_switch,如果获取到了新的进程,进行进程切换

其中,pick_next_task_fair->pick_next_entity,其实就是按红黑树从左到右尝试反馈优先级最高的进程;
然后,当前进程被切换时,也会更新虚拟时间,会在CFS红黑数中比较右侧的地方找到自己的位置,然后一直向左,向左,直到再次被调度。