【Linux】程序地址空间 -- 详解 Linux 2.6 内核进程调度队列 -- 了解

一、程序地址空间回顾

在学习 C/C++ 时,我们知道内存会被分为几个区域:栈区、堆区、全局/静态区、代码区、字符常量区等。但这仅仅是在语言层面上的理解,是远远不够的。

如下空间布局图,请问这是物理内存吗?

不是,下图是进程地址空间。

结论

  1. 进程地址空间不是物理内存。

  2. 进程地址空间会在进程的整个生命周期内一直存在,直到进程退出。

这也就解释了为什么全局/静态变量的生命周期是整个程序,因为全局/静态变量是随着进程一直存在的


二、验证地址空间的基本排布

// checkarea.c
#include <stdio.h>
#include <stdlib.h> // malloc

int g_unval;    // 未初始化数据区
int g_val = 10; // 已初始化数据区

int main(int argc, char* argv[], char* env[])
{
    printf("code addr        : %p\n", main); // 代码区

    printf("\n");
    const char *p = "hello";
    printf("read only        : %p\n", p);    // 字符常量区(只读)

    printf("\n");
    printf("global val       : %p\n", &g_val);   // 已初始化数据区
    printf("global uninit val: %p\n", &g_unval); // 未初始化数据区

    printf("\n");
    char *phead = (char*)malloc(1);
    printf("head addr        : %p\n", phead);  // 堆区(向上增长)

    printf("\n");
    printf("stack addr       : %p\n", &p);     // 栈区(向下增长)
    printf("stack addr       : %p\n", &phead); // 栈区

    printf("\n");
    printf("arguments addr   : %p\n", argv[0]);        // 命令行参数(第一个参数)
    printf("arguments addr   : %p\n", argv[argc-1]); // 命令行参数(最后一个参数)

    printf("\n");
    printf("environ addr     : %p\n", env[0]);         // 环境变量

    return 0;
}

运行结果:


三、虚拟地址和物理地址

定义一个全局变量 g_val,然后创建子进程,父子进程分别打印出变量值和变量地址。

#include <stdio.h>
#include <sys/types.h> // getpid
#include <unistd.h>    // getpid, fork
#include <stdlib.h>    // perror

int g_val = 0; // 全局变量

int main()
{
    printf("before creating a new process, g_val = %d\n", g_val);

    pid_t ret = fork();
    if (ret == 0)
    {
        // child process
        printf(" child - pid: %u, g_val: %d, &g_val: %p\n", getpid(), g_val, &g_val);
    }
    else if (ret > 0)
    {
        // father process
        printf("father - pid: %u, g_val: %d, &g_val: %p\n", getpid(), g_val, &g_val);
    }
    else
    {
        perror("fork");
    }  

    return 0;
}

运行结果:

before creating a new process, g_val = 0
father - pid: 23014, g_val: 0, &g_val: 0x601058
 child - pid: 23015, g_val: 0, &g_val: 0x601058

通过观察可以发现,父子进程打印的变量值和变量地址是一样的,因为创建子进程通常以父进程为模版,父子进程并没有对变量进行进行任何修改。

如果将代码稍加改动:

#include <stdio.h>
#include <sys/types.h> // getpid
#include <unistd.h>    // getpid, fork, sleep
#include <stdlib.h>    // perror

int g_val = 0; // 全局变量

int main()
{
    printf("before creating a new process, g_val = %d\n", g_val);

    pid_t ret = fork();
    if (ret == 0)
    {
        // child process
        g_val = 100; // 在子进程中对变量进行修改
        printf(" child - pid: %u, g_val: %-3d, &g_val: %p\n", getpid(), g_val, &g_val);
    }
    else if (ret > 0)
    {
        // father process
        sleep(3); // 父进程休眠,子进程一定会先退出,让父进程读取变量值和变量地址
        printf("father - pid: %u, g_val: %-3d, &g_val: %p\n", getpid(), g_val, &g_val);
    }
    else
    {
        perror("fork");
    }  

    return 0;
}

运行结果:

before creating a new process, g_val = 0
 child - pid: 25270, g_val: 100, &g_val: 0x601058  # 子进程先退出
father - pid: 25269, g_val: 0  , &g_val: 0x601058  # 父进程休眠3s后退出

子进程肯定先跑完,也就是子进程先修改,完成之后,父进程再读取。

可以发现:父子进程打印的变量值是不一样,但变量地址是一样的

父子进程代码共享,数据各自私有一份(写时拷贝

  • 变量内容不一样,说明父子进程中的变量绝对不是同一个变量
  • 打印的变量地址值是一样的,说明绝对不是物理地址。因为在同一物理地址处,不可能读取出两个不同的值。
  • 我们曾经在 C/C++ 语言或其它语言中学到或看到的地址(比如:取地址),全都是虚拟地址物理地址,用户是一概看不到的,由操作系统统一管理。
  • OS 必须负责将虚拟地址转化成物理地址

注意程序的代码和数据一定是存在物理内存上的。

因为想要运行程序就必须先将代码和数据加载到物理内存中,所以需要操作系统负责将虚拟地址转化成物理地址。

所以之前说 “ 程序的地址空间”  是不准确的,准确来说应该是 “ 进程地址空间”

上图说明:同一个变量打印的地址相同,其实是虚拟地址相同,而内容不同,其实是被映射到了不同的物理地址处。


四、理解地址空间

1、举例

假设有一个富豪,他有 10 亿美元的家产,而他有 3 个私生子,但这 3 个私生子彼此之间并不知道对方的存在。这个富豪对他的每个私生子都说过同一句话:“儿子,这 10 亿的家产未来都是你的”。站在每个私生子的视角来看,每个私生子都认为自己可以拥有 10 亿美元。

如果每个私生子都找父亲一次性要 10 个亿,那么这个富豪是拿不出来的。但实际上这是不可能的,每个私生子找父亲要钱,一般只会几千几万这样一点点去要,那么这个富豪只要有,就一定会给。而如果私生子要的钱太多,富豪不给,私生子也只会认为是父亲不想给。换而言之,这个富豪给每个私生子在大脑中建立一个虚拟的概念:都认为自己拥有 10 亿美元。

类比到计算机中:

  • 富豪 —— 操作系统
  • 私生子 —— 进程
  • 富豪给私生子画的 10 亿家产的 —— 进程的地址空间

通过上述例子,可以得出结论:

  • 操作系统默认会给每个进程构建一个地址空间的概念(比如在 32 位下,把物理内存资源抽象成了从 0x00000000 ~ 0xFFFFFFFF 共 4G 的一个线性的虚拟地址空间
  • 假设系统中有 10 个进程,每个进程都会认为自己有 4G 的物理内存资源。(这里可以理解成 OS 在画大饼)

2、认识地址空间

  • 在 Linux 中,地址空间其实是内核中的一种数据结构
  • 在 Linux 中,OS 除了会为每个进程创建对应的 PCB(即 struct task_struct 结构体),还会创建对应的进程地址空间,即内核中的 struct mm_struct 结构体。

空间的本质无非就是多个区域(栈、堆…)的集合。

那么在 struct mm_struct 结构体中,OS 是如何表述(划分)这些区域的呢?

定义 start 和 end 变量来表示每个区域起始和结束的虚拟地址。然后通过设置这些 start 和 end 的值,对抽象出的这个线性的虚拟地址空间(在 32 位下,是从 0x00000000 ~ 0xFFFFFFFF 共 4G)进行区域划分

struct mm_struct {
    // ...
    
    unsigned long code_start;   // 代码区起始虚拟地址,比如 0x10000000h
    unsigned long code_end;     // 代码区结束虚拟地址,比如 0x00001111h
    
    unsigned long init_start;   // 已初始化数据区
    unsigned long init_end;
    
    unsigned long uninit_start; // 未初始化数据区
    unsigned long uninit_end;
    
    unsigned long heap_start;   // 堆区
    unsigned long heap_end;
    
    // ...
};

3、什么是地址空间

进程地址空间:

地址空间究竟是什么?

地址空间的本质:操作系统让进程看待物理内存的方式,这是抽象出来的一个概念。地址空间是内核中的一种数据结构,即 struct mm_struct 结构体。由 OS 给每个进程创建,这样每个进程都认为自己独占系统内存资源。

划分区域的本质:把线性的地址空间划分成了一个个的区域,通过设置结构体内的 start 和 end 的值来表示区域的起始和结束。(比如栈区和堆区的增长)

为什么要进行区域划分呢?
  1. 可以通过 [start, end] 进行初步判断访问某个虚拟地址时,是否越界访问了
  2. 因为可执行程序在磁盘中是被划分成一个个的区域存储起来的,所以进程的地址空间才有了区域划分这样的概念,方便进程找到代码和数据
  • 虚拟地址的本质:每个区域 [start, end] 之间的各个地址就是虚拟地址,之间的虚拟地址是连续的

五、地址空间和物理内存之间的关系

虚拟地址物理地址之间是通过页表来完成映射的。


六、存在地址空间的原因

直接让进程去访问物理内存不行吗?
  • 早期,操作系统是没有进程地址空间的,这就导致物理内存暴露,恶意程序可以直接通过物理地址来进行内存数据的读取,甚至篡改。
  • 后来,随着操作系统的发展迭代,有了进程地址空间(虚拟地址),由操作系统完成虚拟地址和物理地址之间的转化。

为什么还要存在地址空间呢?

(1)有效的保护物理内存。

因为地址空间和页表是 OS 创建并维护的,也就意味着凡是想使用地址空间和页表进行映射,也就一定要在 OS 的监督之下来进行访问,也保护了物理内存中的所有合法数据,包括各个进程,以及内核的相关有效数据。

在进程内不能非法访问或映射,因为 OS 会进行合法性检测,如果非法则终止进程。
  • 通过划分区域中虚拟地址的起始和结束(即 start 和 end 的值)来判断当前访问的地址是否合法。

比如:如果用户想在某个虚拟地址处写入,但检测到该虚拟地址在字符常量区的 start 到 end 之间,而字符常量区是只读,说明非法越界访问了,OS 会直接终止进程。

char *str = "hello world";
*str = 'H'; // error

  • 通过页表中的权限属性,来判断当前访问的地址是否合法。页表完成了虚拟地址到物理地址之间的映射,而页表中除了有基本的映射关系之外,还可以进行读写等权限相关的管理。

比如:如果用户想在某个虚拟地址处写入,通过页表进行虚拟地址到物理地址的转换时,发现该地址处只有读权限,说明非法访问了,页表拒绝转换,OS 直接终止进程。


(2)内存管理模块进程管理模块在系统层面上进行解耦合。 

操作系统的核心功能:内存管理、进程管理、文件管理、驱动管理。

  • 没有进程地址空间时,内存管理必须得知道所有的进程的生命状态(创建、退出等)才能为每个进程分配和释放相关内存资源。所以内存管理模块进程管理模块强耦合的。
  • 而现在有了进程地址空间,内存管理只需要知道哪些内存区域(page)是被页表映射的(已使用),哪些是没有被页表映射的(未使用),不需要知道每个进程的生命状态。当进程管理想要申请内存资源时,让内存管理通过页表建立映射即可;想要释放内存资源时,通过页表取消映射即可。解耦的本质也就是减少模块与模块之间的关联性,所以就是将内存管理模块进程管理模块进行解耦了。

在物理内存中,是否可以对未来的数据进行任意位置的加载?

可以。

物理内存的分配可以和进程的管理做到没有关系。

在 C/C++ 语言上 new/malloc 出一块新的空间时,本质是在哪里申请空间的呢?

 虚拟地址空间。

如果申请了空间,但不立马使用这块空间, 是不是对空间造成了浪费呢?

是的。

所以本质上,(因为有地址空间的存在,所以上层申请空间,缺页中断:其实是在地址空间上申请的,物理内存可以甚至一个字节都不给。而当我们真正进行对物理地址空间访问时,才执行内存的相关管理算法来申请内存,构建页表映射关系)然后再进行内存的访问。

括号内的部分完全由 OS 自动完成,用户,包括进程完全 0 感知。

  • 在分配内存时采用延迟分配的策略来提高整机的效率。(几乎内存的有效使用率是 100%)

(3)通过页表映射到不同的有序区域来实现进程的独立性。
  • 在进程的视角,所有的内存分别都可以是有序的。
  • 让每个进程以同样的方式来看待代码和数据。(这样对于进程的设计是非常好的)

可执行程序,在磁盘中是被划分成一个个的区域存储起来的(比如代码 .txt、已初始化数据 .data、未初始化数据 .bss 等等)。

因为可执行程序形成时,有一个链接的过程,会把用户代码和库的代码合并在一起,把用户数据和库的数据合并在一起。否则可执行程序的代码和数据如果是混着存放在一起的,会导致链接过程变得很复杂。所以进程的地址空间才有了区域划分这样的概念,方便进程找到代码和数据。

分析:

如图,代码被零散的加载到了内存的各个位置。如果直接让进程去找到代码是非常困难的,尤其是找到代码的起始和结束位置。所以我们在进程的地址空间中划分出一个个区域,再通过页表把内存中的各个位置的代码给整合到一起,使代码的物理地址变成线性的虚拟地址了。然后进程通过其对应地址空间中的代码区(区域中虚拟地址是连续的)可以很方便的找到代码。同时 CPU 也方便执行代码(虚拟地址是连续的,这样 PC 指针才能进行加 1 的操作,得到下一条指今的地址,CPU 才能从上到下顺序执行指令)。

  • 地址空间 + 页表的存在可以将内存分布有序化
  • 结合(2),进程要访问物理内存中的数据和代码,可能目前并没有在物理内存中。同样的,也可以让不同的进程映射到不同的物理内存,便很容易做到进程独立性的实现。
  • 进程的独立性可以通过进程空间 + 页表的方式实现。

好处

  • 不用在物理内存中找一块连续的区域。
  • 站在进程的角度,所有进程的代码(二进制指令)存放的区域,虚拟地址是连续的,可以被顺序执行。(即使物理内存上有可能不连续)

七、重新理解什么是挂起

进程和程序有什么区别呢?

  • 加载的本质就是创建进程。
那么是否必须立刻将所有程序的代码和数据加载到内存中,并创建内核数据结构建立映射关系?

不是。

如果在最极端的情况下,只有内核结构被创建出来了(新建状态)。当真正被调度/执行代码时,才把外设加载内存里,然后再执行代码。

  • 理论上,可以实现对程序的分批加载。
如果物理内存只有 4G,有一个游戏 16G,能否运行?

可以运行。

CPU 无论运行多大的程序,都需要从头到尾执行每一行指令。即使物理内存有 32G,也不会一次性把 16G 的程序加载进来(因为内存资源还需要分配给其它进程),而是采用延时加载比如先加载 200M 进来,执行完了再覆盖式的加载 200M 进来,然后再执行。所以如果物理内存比较小,用户可能会感到游戏卡顿。

  • 加载的本质就是换入的过程。
既然可以分批加载,那可以分批换出吗?

可以。

甚至这个进程短时间不会再被执行,比如挂起 / 阻塞。

  • 也就相当于其对应的代码和数据占着空间却不创造价值,所以 OS 就可以将它换出,一旦被换出,那么此时这个进程就叫被挂起

八、Linux2.6 内核进程调度队列

1、Linux2.6 内核中进程队列的数据结构


2、一个 CPU 拥有一个 runqueue

如果有多个 CPU 就要考虑进程个数的负载均衡问题。

3、优先级

  • 普通优先级:100~139(我们都是普通的优先级,想想 nice 值的取值范围,可与之对应)
  • 实时优先级:0~99(不关心)

4、活动队列

  • 时间片还没有结束的所有进程都按照优先级放在该队列。
  • nr_active:总共有多少个运行状态的进程。
  • queue[140]:一个元素就是一个进程队列,相同优先级的进程按照 FIFO 规则进行排队调度,所以数组下标就是优先级。
  • 从该结构中,选择一个最合适的进程,过程怎么回事的呢?
  1. 从 0 下表开始遍历 queue[140]。
  2. 找到第一个非空队列,该队列必定为优先级最高的队列。
  3. 拿到选中队列的第一个进程,开始运行,调度完成。
  4. 遍历 queue[140] 时间复杂度是常数,但还是太低效了。
  • bitmap[5]:一共 140 个优先级,140 个进程队列,为了提高查找非空队列的效率,就可以用 5*32 个比特位表示队列是否为空,这样便可以大大提高查找效率。

5、过期队列

  • 过期队列和活动队列结构一模一样。
  • 过期队列上放置的进程,都是时间片耗尽的进程。
  • 当活动队列上的进程都被处理完毕之后,对过期队列的进程进行时间片重新计算。

6、active 指针和 expired 指针

  • active 指针永远指向活动队列。
  • expired 指针永远指向过期队列。
  • 可是活动队列上的进程会越来越少,过期队列上的进程会越来越多,因为进程时间片到期时一直都存在的。
  • 但在合适的时候,只要能够交换 active 指针和 expired 指针的内容,就相当于有具有了一批新的活动进程。

7、总结

在系统当中查找一个最合适调度的进程的时间复杂度是一个常数,不随着进程增多而导致时间成本增加,我们称之为进程调度 O(1) 算法。
【Linux】Linux 的进程优先级 NI 和 PR-CSDN博客

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/392133.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

LV.23 D3 STM32 GPIO口操作平台介绍 学习笔记

一、GPIO的基本概念及寄存器介绍 1.1 GPIO接口简介 通用输入输出接口GPIO是嵌入式系统、单片机开发过程中最常用的接口&#xff0c;用户可以通过编程灵活的对接口进行控制&#xff0c;实现对电路板上LED、数码管、按键等常用设备控制驱动&#xff0c;也可以作为串口的数据收发…

文件上传漏洞--Upload-labs--Pass02--Content-Type绕过

一、什么是 Content-Type 我们在上传文件时利用 Burpsuite 进行抓包&#xff0c;如下图所示&#xff1a; 上传文件后台的源代码可能会对 Content-Type 进行规定&#xff0c;设置白名单 或 黑名单&#xff0c;这时就要利用Content-Type绕过上传含有恶意代码的 php文件。 二、代…

2023年中国数据智能管理峰会(DAMS上海站2023):核心内容与学习收获(附大会核心PPT下载)

随着数字经济的飞速发展&#xff0c;数据已经渗透到现代社会的每一个角落&#xff0c;成为驱动企业创新、提升治理能力、促进经济发展的关键要素。在这样的背景下&#xff0c;2023年中国数据智能管理峰会&#xff08;DAMS上海站2023&#xff09;应运而生&#xff0c;汇聚了众多…

解锁Spring Boot中的设计模式—03.委派模式:探索【委派模式】的奥秘与应用实践!

委派模式 文章目录 委派模式1.简述**应用场景****优缺点****业务场景示例** 2.类图3.具体实现3.1.自定义注解3.2.定义抽象委派接口3.3.定义具体执行者3.4.定义委派者(统一管理委派任务)3.5.定义委派者管理类 4.测试4.1.controller层4.2.测试不同场景4.2.1.测试生产部门计算费用…

排序前言冒泡排序

目录 排序应用 常见的排序算法 BubbleSort冒泡排序 整体思路 图解分析 ​ 代码实现 每趟 写法1 写法2 代码NO1 代码NO2优化 时间复杂度 排序概念 排序&#xff1a;所谓排序&#xff0c;就是使一串记录&#xff0c;按照其中的某个或某些关键字的大小&#xff0c;递…

python in Vscode

背景 对于后端的语言选择&#xff1a; python&#xff0c;java&#xff0c;JavaScript备选。 选择Python 原因&#xff1a;可能是非IT专业的人中&#xff0c;会Python的人比较多。 目的 之前使用的IDE是VSCODE&#xff0c;在WSL的环境下使用。现在需要在在WSL的VSCODE下使…

【开源】SpringBoot框架开发创意工坊双创管理系统

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 管理员端2.2 Web 端2.3 移动端 三、系统展示四、核心代码4.1 查询项目4.2 移动端新增团队4.3 查询讲座4.4 讲座收藏4.5 小程序登录 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的创意工坊双创管理…

鸿蒙(HarmonyOS)项目方舟框架(ArkUI)之NavRouter组件

鸿蒙&#xff08;HarmonyOS&#xff09;项目方舟框架&#xff08;ArkUI&#xff09;之NavRouter组件 一、操作环境 操作系统: Windows 10 专业版、IDE:DevEco Studio 3.1、SDK:HarmonyOS 3.1 二、NavRouter组件 导航组件&#xff0c;默认提供点击响应处理&#xff0c;不需要…

MySQL数据库⑪_C/C++连接MySQL_发送请求

目录 1. 下载库文件 2. 使用库 3. 链接MySQL函数 4. C/C链接示例 5. 发送SQL请求 6. 获取查询结果 本篇完。 1. 下载库文件 要使用C/C连接MySQL&#xff0c;需要使用MySQL官网提供的库。 进入MySQL官网选择适合自己平台的mysql connect库&#xff0c;然后点击下载就行…

相机图像质量研究(27)常见问题总结:补光灯以及遮光罩对成像的影响--遮光罩

系列文章目录 相机图像质量研究(1)Camera成像流程介绍 相机图像质量研究(2)ISP专用平台调优介绍 相机图像质量研究(3)图像质量测试介绍 相机图像质量研究(4)常见问题总结&#xff1a;光学结构对成像的影响--焦距 相机图像质量研究(5)常见问题总结&#xff1a;光学结构对成…

视频生成模型作为世界模拟器

我们探索了在视频数据上大规模训练生成模型。具体来说&#xff0c;我们联合训练文本条件扩散模型&#xff0c;处理不同持续时间、分辨率和宽高比的视频和图像。我们利用一种在时空补丁上操作视频和图像潜码的transformer架构。我们最大的模型&#xff0c;Sora&#xff0c;能够生…

【初始RabbitMQ】了解和安装RabbitMQ

RabbitMQ的概念 RabbitMQ是一个消息中间件&#xff1a;他可以接受并转发消息。例如你可以把它当做一个快递站点&#xff0c;当你要发送一个包 裹时&#xff0c;你把你的包裹放到快递站&#xff0c;快递员最终会把你的快递送到收件人那里&#xff0c;按照这种逻辑 RabbitMQ 是 …

数据的力量:构筑现代大型网站之数据库基础与应用

目录 数据库基础知识--前言 大型网站架构特点 DBA数据库管理员 什么是数据? 数据存储 什么是数据库 数据表的概念 为什么需要mysql这样的数据库管理工具&#xff1f;★ DBMS 收费数据库与免费数据库 运维和数据库 开发与运维的不同阶段 数据库类别 数据库具体应用…

【Linux】进程信号的保存 | 自定义捕捉

文章目录 三、信号的阻塞&#xff08;信号的保存&#xff09;1. 信号相关其他常见概念2. 在内核中的表示3. sigset_t类型4. 信号集操作函数函数列表注意事项 5. 读取/修改block位图 - sigprocmask6. 读取pending位图 - sigpending 四、信号捕捉1. 信号捕捉的初步认识自定义捕捉…

安卓AndroidStdio控制台乱码解决

安卓AndroidStdio控制台乱码解决 情况&#xff1a; 在AndroidStudio中新建了一个Java Module&#xff0c;但是点击 Run ‘app’之后&#xff0c;Build Output 控制台输出的中文都是乱码&#xff0c;都是问号一样的字符 第一个解决方案 File Encodings 改为UTF-8&#xff1f; …

鸿蒙语言ArkTS(更好的生产力与性能)

ArkTS是鸿蒙生态的应用开发语言 ArkTS提供了声明式UI范式、状态管理支持等相应的能力&#xff0c;让开发者可以以更简洁、更自然的方式开发应用。 同时&#xff0c;它在保持TypeScript&#xff08;简称TS&#xff09;基本语法风格的基础上&#xff0c;进一步通过规范强化静态检…

5G——小区搜索流程

小区搜索流程 小区搜索目标&#xff1a;读取到SIB1. 小区搜索流程概述&#xff1a;SIB1在PDSCH信道承载&#xff0c;承载SIB1的信道在哪个位置由PDCCH告诉&#xff0c;而PDCCH的基本信息由MIB告诉&#xff0c;MIB信息由广播信道PBCH广播出去&#xff0c;物理信道解调需要解调…

Codeforces Round 926 (Div. 2) C. Sasha and the Casino (Java)

Codeforces Round 926 (Div. 2) CC. Sasha and the Casino (Java) 比赛链接&#xff1a;Codeforces Round 926 (Div. 2) C题传送门&#xff1a;C. Sasha and the Casino 题目&#xff1a;C. Sasha and the Casino **Example ** input 2 1 7 2 1 1 2 3 15 3 3 6 4 4 5 5 4 7…

EXCEL中不错的xlookup函数

excel中一般要经常用vlookup函数&#xff0c;但其实经常麻烦要正序&#xff0c;从左边到右边&#xff0c;还要数列&#xff0c;挺麻烦的&#xff0c;xlookup的函数还不错&#xff0c;有个不错的一套视频介绍,B站的&#xff0c;地址是&#xff1a;XLOOKUP函数基础用法&#xff0…

BIG DATA —— 大数据时代

大数据时代 [英] 维克托 迈尔 — 舍恩伯格 肯尼斯 库克耶 ◎ 著 盛杨燕 周涛◎译 《大数据时代》是国外大数据研究的先河之作&#xff0c;本书作者维克托迈尔舍恩伯格被誉为“大数据商业应用第一人”&#xff0c;他在书中前瞻性地指出&#xff0c;大数据带来的信息…