• 学内核之八:Linux内核的smp_processor_id是如何实现的


    smp_processor_id在内核中大量使用。这个接口用于获取当前代码(就是调用该接口所在位置的代码)当前在哪个逻辑CPU上运行。

    这篇文章探讨这样一个问题,就是上述接口是如何实现的。

    我第一次了解到这个接口时,以为是通过读取CPU内部的特定寄存器或者通过特定指令实现的。为了验证这个想法,就简单看了下代码,发现在ARM32下,是通过当前内核线程信息结构的cpu字段来获取的。如下:

    1. ./include/linux/smp.h:
    2. # define smp_processor_id() raw_smp_processor_id()
    3. //smp_processor_id是raw_smp_processor_id的别名
    4. //而raw_processor_id则实现为如下接口
    5. ./arch/arm/include/asm/smp.h:
    6. #define raw_smp_processor_id() (current_thread_info()->cpu)

    看到这个结果,我有点纳闷,怎么可以通过这个字段就可以知道当前自己在哪个逻辑CPU上呢?那这个疑问的答案就在这个变量啥时候赋值了。为了这个赋值,曾开启了一段艰苦的探索之旅。

    其实,要追踪这个赋值,我们需要清楚CPU的执行上下文都有哪些。

    对于任何一个CPU核心,当前执行的代码,可能属于:
    1 用户代码,属于某一个用户进程或线程,至于说进程还是线程,关系不大。
    2 用户切换到内核过程,比如系统调用
    3 内核逻辑执行,要么属于用户进程,要么属于独立的内核线程
    4 中断,要么从内核态切换,要么从用户态切换
    5 异常,要么内核态触发,要么用户态触发
    6 信号处理。从内核返回用户空间时,如果有待处理的信号,那么内核需要让用户代码恢复执行前先执行信号注册的部分。

    可以看到,无论在那种情况下,当前执行的代码都是有主的,也就是有上下文的,要么是用户线程,要么是内核线程,要么是各种异常,总之是有来源的。这是非常关键的信息。

    如果CPU核心开始运行的时候,知道自己是在哪个核心上,那么从最初的idle任务开始,核心就可以传递这个编号信息。

    首先,idle任务通过人为设置,确定编号,就是创世之源。

    之后,从idle开始,无论是切换到上述6种中的哪一种场景,如果我们都能够在切换时传递编号,就像是现实世界的代代相传

    那么,任何时候,代码要知道自己被执行的CPU,就只需要查找自己处在什么上下文中,然后看看这个上下文的CPU编号是什么,就最终能够确定自己在那个核心上运行。

    整个流程就是,当前代码调用接口获取CPU编号--接口查找当前上下文编号--当前上下文编号来自切换者传递--切换者来自更早的切换者--最初的idle由人为设定

    只要保证上述的调用链条正确,那么查找结果就是正确的。

    那么问题来了,编号的本源来自哪里,就是最初给第一个内核态的idle线程栈赋值编号的地方在哪里?

    首先,上电启动的核心一般都是编号为0的核心,这个是CPU设计时候决定的,所以整个内核的初始化,基本都是由编号为0的核心完成的。
    其他核心在核心0完成必要的初始化工作后,唤醒,开始接任务,共同分担系统负载。

    所以,其他核心启动前,运行代码的核心编号是0,这个好确定。待其他核心启动后,如何知道各个核心的编号?

    我们先来看看内核启动的日志

    1. Booting Linux on physical CPU 0x0
    2. Initializing cgroup subsys cpu
    3. Linux version 3.18.20 (root@ubuntu) (gcc version 4.9.4 20150629 (prerelease) (Hisilicon_v600_20180525) ) #19 SMP Wed May 29 19:23:24 PDT 2019
    4. CPU: ARMv7 Processor [410fc075] revision 5 (ARMv7), cr=10c5387d
    5. CPU: PIPT / VIPT nonaliasing data cache, VIPT aliasing instruction cache
    6. Machine model: Hisilicon HI3536C DEMO Board
    7. Memory policy: Data cache writealloc
    8. On node 0 totalpages: 65536
    9. free_area_init_node: node 0, pgdat c074ee00, node_mem_map cfdf7000
    10.   Normal zone: 512 pages used for memmap
    11.   Normal zone: 0 pages reserved
    12.   Normal zone: 65536 pages, LIFO batch:15
    13. PERCPU: Embedded 9 pages/cpu @cfddc000 s6144 r8192 d22528 u36864
    14. pcpu-alloc: s6144 r8192 d22528 u36864 alloc=9*4096
    15. pcpu-alloc: [0] 0 [0] 1 
    16. Built 1 zonelists in Zone order, mobility grouping on.  Total pages: 65024
    17. Kernel command line: mem=256M console=ttyAMA0,115200 root=/dev/mtdblock2 rootfstype=jffs2 rw mtdparts=hi_sfc:1M(boot),4M(kernel),27M(rootfs)
    18. PID hash table entries: 1024 (order: 0, 4096 bytes)
    19. Dentry cache hash table entries: 32768 (order: 5, 131072 bytes)
    20. Inode-cache hash table entries: 16384 (order: 4, 65536 bytes)
    21. Memory: 251976K/262144K available (5483K kernel code, 230K rwdata, 1496K rodata, 240K init, 297K bss, 10168K reserved, 0K highmem)
    22. Virtual kernel memory layout:
    23.     vector  : 0xffff0000 - 0xffff1000   (   4 kB)
    24.     fixmap  : 0xffc00000 - 0xffe00000   (2048 kB)
    25.     vmalloc : 0xd0800000 - 0xff000000   ( 744 MB)
    26.     lowmem  : 0xc0000000 - 0xd0000000   ( 256 MB)
    27.     pkmap   : 0xbfe00000 - 0xc0000000   (   2 MB)
    28.     modules : 0xbf000000 - 0xbfe00000   (  14 MB)
    29.       .text : 0xc0008000 - 0xc06d9070   (6981 kB)
    30.       .init : 0xc06da000 - 0xc0716000   ( 240 kB)
    31.       .data : 0xc0716000 - 0xc074fb40   ( 231 kB)
    32.        .bss : 0xc074fb40 - 0xc079a1fc   ( 298 kB)
    33. SLUB: HWalign=64, Order=0-3, MinObjects=0, CPUs=2, Nodes=1
    34. Hierarchical RCU implementation.
    35.         RCU restricting CPUs from NR_CPUS=4 to nr_cpu_ids=2.
    36. RCU: Adjusting geometry for rcu_fanout_leaf=16, nr_cpu_ids=2
    37. NR_IRQS:16 nr_irqs:16 16
    38. sched_clock: 32 bits at 3000kHz, resolution 333ns, wraps every 1431655765682ns
    39. Console: colour dummy device 80x30
    40. Calibrating delay loop... 2580.48 BogoMIPS (lpj=1290240)
    41. pid_max: default: 32768 minimum: 301
    42. Mount-cache hash table entries: 1024 (order: 0, 4096 bytes)
    43. Mountpoint-cache hash table entries: 1024 (order: 0, 4096 bytes)
    44. CPU: Testing write buffer coherency: ok
    45. CPU0: thread -1, cpu 0, socket 0, mpidr 80000000
    46. Setting up static identity map for 0x8053c180 - 0x8053c1d8
    47. CPU1: Booted secondary processor
    48. CPU1: thread -1, cpu 1, socket 0, mpidr 80000001
    49. Brought up 2 CPUs
    50. SMP: Total of 2 processors activated (5173.24 BogoMIPS).
    51. CPU: All CPU(s) started in SVC mode.
    52. devtmpfs: initialized
    53. VFP support v0.3: implementor 41 architecture 2 part 30 variant 7 rev 5

    可以看到,SMP其他核心启动在驱动之前,在核心0完成必要的初始化boot操作后

    smp_init中,这个CPU编号是自己生成,等到之后的代码,系统都开始采用smp_processor_id获取编号了。

    1. /* Called by boot processor to activate the rest. */
    2. void __init smp_init(void)
    3. {
    4.     unsigned int cpu;
    5.     idle_threads_init();
    6.     /* FIXME: This should be done in userspace --RR */
    7.     for_each_present_cpu(cpu) {
    8.         if (num_online_cpus() >= setup_max_cpus)
    9.             break;
    10.         if (!cpu_online(cpu))
    11.             cpu_up(cpu);
    12.     }
    13.     /* Any cleanup work */
    14.     smp_announce();
    15.     smp_cpus_done(setup_max_cpus);
    16. }


    这时候已经可以拿取编号了

    1. /**
    2. * idle_threads_init - Initialize idle threads for all cpus
    3. */
    4. void __init idle_threads_init(void)
    5. {
    6. unsigned int cpu, boot_cpu;
    7. boot_cpu = smp_processor_id();
    8. for_each_possible_cpu(cpu) {
    9. if (cpu != boot_cpu)
    10. idle_init(cpu);
    11. }
    12. }

    但这里是boot CPU,所以是零,其他CPU,需要进一步看idle_init

    1. /**
    2. * idle_init - Initialize the idle thread for a cpu
    3. * @cpu: The cpu for which the idle thread should be initialized
    4. *
    5. * Creates the thread if it does not exist.
    6. */
    7. static inline void idle_init(unsigned int cpu)
    8. {
    9. struct task_struct *tsk = per_cpu(idle_threads, cpu);
    10. if (!tsk) {
    11. tsk = fork_idle(cpu);
    12. if (IS_ERR(tsk))
    13. pr_err("SMP: fork_idle() failed for CPU %u\n", cpu);
    14. else
    15. per_cpu(idle_threads, cpu) = tsk;
    16. }
    17. }
    18. struct task_struct *fork_idle(int cpu)
    19. {
    20. struct task_struct *task;
    21. task = copy_process(CLONE_VM, 0, 0, NULL, &init_struct_pid, 0);
    22. if (!IS_ERR(task)) {
    23. init_idle_pids(task->pids);
    24. init_idle(task, cpu);
    25. }
    26. return task;
    27. }
    28. /**
    29. * init_idle - set up an idle thread for a given CPU
    30. * @idle: task in question
    31. * @cpu: cpu the idle task belongs to
    32. *
    33. * NOTE: this function does not set the idle thread's NEED_RESCHED
    34. * flag, to make booting more robust.
    35. */
    36. void init_idle(struct task_struct *idle, int cpu)
    37. {
    38. struct rq *rq = cpu_rq(cpu);
    39. unsigned long flags;
    40. raw_spin_lock_irqsave(&rq->lock, flags);
    41. __sched_fork(0, idle);
    42. idle->state = TASK_RUNNING;
    43. idle->se.exec_start = sched_clock();
    44. do_set_cpus_allowed(idle, cpumask_of(cpu));
    45. /*
    46. * We're having a chicken and egg problem, even though we are
    47. * holding rq->lock, the cpu isn't yet set to this cpu so the
    48. * lockdep check in task_group() will fail.
    49. *
    50. * Similar case to sched_fork(). / Alternatively we could
    51. * use task_rq_lock() here and obtain the other rq->lock.
    52. *
    53. * Silence PROVE_RCU
    54. */
    55. rcu_read_lock();
    56. __set_task_cpu(idle, cpu);
    57. rcu_read_unlock();
    58. rq->curr = rq->idle = idle;
    59. idle->on_rq = TASK_ON_RQ_QUEUED;
    60. #if defined(CONFIG_SMP)
    61. idle->on_cpu = 1;
    62. #endif
    63. raw_spin_unlock_irqrestore(&rq->lock, flags);
    64. /* Set the preempt count _outside_ the spinlocks! */
    65. init_idle_preempt_count(idle, cpu);
    66. /*
    67. * The idle tasks have their own, simple scheduling class:
    68. */
    69. idle->sched_class = &idle_sched_class;
    70. ftrace_graph_init_idle_task(idle, cpu);
    71. vtime_init_idle(idle, cpu);
    72. #if defined(CONFIG_SMP)
    73. sprintf(idle->comm, "%s/%d", INIT_TASK_COMM, cpu);
    74. #endif
    75. }
    76. static inline void __set_task_cpu(struct task_struct *p, unsigned int cpu)
    77. {
    78. set_task_rq(p, cpu);
    79. #ifdef CONFIG_SMP
    80. /*
    81. * After ->cpu is set up to a new value, task_rq_lock(p, ...) can be
    82. * successfuly executed on another CPU. We must ensure that updates of
    83. * per-task data have been completed by this moment.
    84. */
    85. smp_wmb();
    86. task_thread_info(p)->cpu = cpu;
    87. p->wake_cpu = cpu;
    88. #endif
    89. }

    我们注意到,最后这里,task_thread_info(p)->cpu = cpu;

    这里函数调用过程都传递了参数CPU,也就是说,对SMP中的每一个其他逻辑CPU,创建了idle task,并将task的CPU设置为参数传递的编号。注意,这里,其他CPU还没有运行,这些编号是预置的,也就是编号设置为1的idle,到时候是需要CPU编号为1的核心来执行的。这一点又是如何做到的。

    这里先把前面的流程补充上。idle的创建来自fork

    1. static noinline void __init_refok rest_init(void)
    2. {
    3. int pid;
    4. rcu_scheduler_starting();
    5. /*
    6. * We need to spawn init first so that it obtains pid 1, however
    7. * the init task will end up wanting to create kthreads, which, if
    8. * we schedule it before we create kthreadd, will OOPS.
    9. */
    10. kernel_thread(kernel_init, NULL, CLONE_FS);
    11. numa_default_policy();
    12. pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
    13. rcu_read_lock();
    14. kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
    15. rcu_read_unlock();
    16. complete(&kthreadd_done);
    17. /*
    18. * The boot idle thread must execute schedule()
    19. * at least once to get things moving:
    20. */
    21. init_idle_bootup_task(current);
    22. schedule_preempt_disabled();
    23. /* Call into cpu_idle with preempt disabled */
    24. cpu_startup_entry(CPUHP_ONLINE);
    25. }

    注意到,kernel_init是在内核线程中执行的,此时是boot CPU执行线程,从idle线程fork而来

    1. /*
    2. * Create a kernel thread.
    3. */
    4. pid_t kernel_thread(int (*fn)(void *), void *arg, unsigned long flags)
    5. {
    6. return do_fork(flags|CLONE_VM|CLONE_UNTRACED, (unsigned long)fn,
    7. (unsigned long)arg, NULL, NULL);
    8. }

    合起来,整个调用栈是
    rest_init
    kernel_init
    kernel_init_freeable
    smp_init
    idle_threads_init
    idle_init
    fork_idle
    init_idle
    __set_task_cpu

    到目前,我们补充了完整的CPU编号设置路径。
     

    现在再次回到上面的问题,如何让编号为 i 核心的CPU执行编号为 i 的idle线程?

    1 上电后,boot CPU 也就是CPU0先运行,其他CPU处于等待状态,使用wfi指令,arm平台

    2 CPU0完成准备工作后,通知其他CPU工作,这是通过核间中断发送的,sev指令,arm平台

    3 其他CPU接收到事件后,就可以从配置的地址运行代码了关键就在这里

    4 其他核心运行的代码最终都会关联到核心自己的idle任务,无论是通过直接写内存地址还是跳转方式。

    5 这样,主CPU就可以将从CPU的idle任务的CPU编号(上面堆栈记录)和从CPU的启动地址对应起来,也就是 i 号CPU的启动地址在其 i 偏移地址处

    最终,从CPU到自己的idle任务代码段去执行,每个从CPU的代码段不一样,这个是主CPU配置的,自然主CPU得知道自己配置的是那个从CPU的代码段,那么也就知道配置那个编号。

    1. Thread 1 hit Breakpoint 3, psci_boot_secondary (cpu=1, idle=0xdb09c800) at arch/arm/kernel/psci_smp.c:54
    2. 54 if (psci_ops.cpu_on)
    3. (gdb) bt
    4. #0 psci_boot_secondary (cpu=1, idle=0xdb09c800) at arch/arm/kernel/psci_smp.c:54
    5. #1 0xc030fde4 in __cpu_up (cpu=1, idle=0xdb09c800) at arch/arm/kernel/smp.c:163
    6. #2 0xc0349478 in bringup_cpu (cpu=1) at kernel/cpu.c:530
    7. #3 0xc0349ae8 in cpuhp_invoke_callback (cpu=1, state=CPUHP_BRINGUP_CPU, bringup=<optimized out>, node=<optimized out>, lastp=0x0) at kernel/cpu.c:170
    8. #4 0xc034ae8c in cpuhp_up_callbacks (target=<optimized out>, st=<optimized out>, cpu=<optimized out>) at kernel/cpu.c:584
    9. #5 _cpu_up (cpu=1, tasks_frozen=<optimized out>, target=<optimized out>) at kernel/cpu.c:1192
    10. #6 0xc034afa4 in do_cpu_up (cpu=1, target=CPUHP_ONLINE) at kernel/cpu.c:1228
    11. #7 0xc034afbc in cpu_up (cpu=<optimized out>) at kernel/cpu.c:1236
    12. #8 0xc14252ac in smp_init () at kernel/smp.c:578
    13. #9 0xc14011a0 in kernel_init_freeable () at init/main.c:1140
    14. #10 0xc0e7be2c in kernel_init (unused=<optimized out>) at init/main.c:1064
    15. #11 0xc03010e8 in ret_from_fork () at arch/arm/kernel/entry-common.S:158
    16. Backtrace stopped: previous frame identical to this frame (corrupt stack?)
    17. (gdb) bt
    18. #0 psci_cpu_on (cpuid=2, entry_point=1076896960) at drivers/firmware/psci.c:194
    19. #1 0xc0316430 in psci_boot_secondary (cpu=<optimized out>, idle=<optimized out>) at ./arch/arm/include/asm/memory.h:323
    20. #2 0xc030fde4 in __cpu_up (cpu=2, idle=0xdb09ce00) at arch/arm/kernel/smp.c:163
    21. #3 0xc0349478 in bringup_cpu (cpu=2) at kernel/cpu.c:530
    22. #4 0xc0349ae8 in cpuhp_invoke_callback (cpu=2, state=CPUHP_BRINGUP_CPU, bringup=<optimized out>, node=<optimized out>, lastp=0x0) at kernel/cpu.c:170
    23. #5 0xc034ae8c in cpuhp_up_callbacks (target=<optimized out>, st=<optimized out>, cpu=<optimized out>) at kernel/cpu.c:584
    24. #6 _cpu_up (cpu=2, tasks_frozen=<optimized out>, target=<optimized out>) at kernel/cpu.c:1192
    25. #7 0xc034afa4 in do_cpu_up (cpu=2, target=CPUHP_ONLINE) at kernel/cpu.c:1228
    26. #8 0xc034afbc in cpu_up (cpu=<optimized out>) at kernel/cpu.c:1236
    27. #9 0xc14252ac in smp_init () at kernel/smp.c:578
    28. #10 0xc14011a0 in kernel_init_freeable () at init/main.c:1140
    29. #11 0xc0e7be2c in kernel_init (unused=<optimized out>) at init/main.c:1064
    30. #12 0xc03010e8 in ret_from_fork () at arch/arm/kernel/entry-common.S:158
    31. Backtrace stopped: previous frame identical to this frame (corrupt stack?)

    以上是qemu模拟里面,启动从CPU的部分堆栈。(图片来自arm官方)

     

  • 相关阅读:
    Linux内核网络设备驱动
    网页中嵌套网页制作方法
    用于Linux日常系统管理任务地sed命令解析
    Java的File文件操作案例汇总
    众和策略:尾盘5分钟拉升意味着什么?
    XSS 攻击是什么?
    Java基础练习题---类型转换、双分支、多分支、switch、for
    Ubuntu 软件包管理工具 —— dkpg、apt(dpkg常用指令、apt 软件源的配置)
    日志轮转logrotate
    P03 注解
  • 原文地址:https://blog.csdn.net/wwwyue1985/article/details/126414070