Linux Kernel 6

news2025/5/25 16:35:08

clone 系统调用(The clone system call)

在 Linux 中,使用 clone() 系统调用来创建新的线程或进程。fork() 系统调用和 pthread_create() 函数都基于 clone() 的实现。

clone() 系统调用允许调用者决定哪些资源应该与父进程共享,哪些应该被复制或隔离。


📌 clone() 系统调用的标志

clone() 通过一组标志来控制父子进程或线程之间的资源共享。常见的标志包括:

  • C L O N E _ F I L E S CLONE\_FILES CLONE_FILES:共享文件描述符表,意味着子进程或子线程将共享父进程的文件描述符(打开的文件)。
  • C L O N E _ V M CLONE\_VM CLONE_VM:共享地址空间,意味着子进程或子线程将与父进程共享同一个虚拟内存地址空间。
  • C L O N E _ F S CLONE\_FS CLONE_FS:共享文件系统信息,如根目录和当前目录。
  • C L O N E _ N E W N S CLONE\_NEWNS CLONE_NEWNS:不共享挂载命名空间,意味着子进程或子线程将有自己的挂载点,不与父进程共享。
  • C L O N E _ N E W I P C CLONE\_NEWIPC CLONE_NEWIPC:不共享 IPC 命名空间(例如,System V IPC 对象、POSIX 消息队列等)。
  • C L O N E _ N E W N E T CLONE\_NEWNET CLONE_NEWNET:不共享网络命名空间(例如,网络接口、路由表等)。

🧠 重要概念解释

  • f o r k ( ) fork() fork():系统调用,用于创建新进程,复制父进程的所有资源,形成父子进程。
  • p t h r e a d _ c r e a t e ( ) pthread\_create() pthread_create():函数,用于在用户空间创建新线程。通常与线程库(如 POSIX 线程库)配合使用。
  • 命名空间(Namespace):在 Linux 中,命名空间用于隔离系统资源。不同的命名空间提供进程间的隔离,例如文件系统、IPC、网络等命名空间。

✅ 创建线程与进程的区别

  • 如果使用 CLONE_FILES | CLONE_VM | CLONE_FS 组合标志,调用者会创建一个新线程,因为它共享父进程的文件描述符表、地址空间和文件系统信息。
  • 如果不使用这些标志,调用者将创建一个新进程,因为新进程将拥有自己的资源,如地址空间、文件描述符表等。

命名空间与“容器”技术(Namespaces and “Containers”)

“容器”是一种轻量级的虚拟机形式,它们共享同一个内核实例,与传统的虚拟化技术不同,传统虚拟化技术通过虚拟机监视器(hypervisor)运行多个虚拟机(VM),每个虚拟机都有一个独立的内核实例。


📌 容器技术的例子

  • LXC(Linux Containers):允许运行轻量级的“虚拟机”,并提供类似虚拟化的隔离功能。
  • Docker:一种专门用于运行单一应用程序的容器技术,广泛用于开发和部署应用。

容器技术依赖于几个内核特性,其中之一就是命名空间(namespace)。命名空间通过隔离不同的资源,避免它们在全局范围内可见。


🔒 命名空间的作用

命名空间为容器提供了资源隔离,使得容器中的进程不会被其他容器的进程所干扰。比如:

  • 没有容器时,系统中的所有进程都在 /proc 中可见;
  • 有容器时,一个容器中的进程不会被其他容器看到(在 /proc 中不可见,也无法被杀死)。

🧠 关键概念解释

  • 命名空间(Namespace):命名空间用于隔离系统资源,使得在不同的命名空间中,资源(如进程、网络接口、挂载点等)是独立的,互不干扰。Linux 支持多种类型的命名空间,包括进程 ID(PID)命名空间、网络命名空间、文件系统命名空间等。

  • 容器(Container):容器是一种通过利用命名空间和其他内核特性(如控制组cgroup)实现的轻量级虚拟化技术。容器共享主机的内核,但它们的资源(如进程、文件系统、网络等)是隔离的。

  • struct nsproxy 结构体:用于管理和分组资源类型的内核数据结构。它是实现命名空间隔离的关键,支持的命名空间类型包括 IPC、网络、PID、挂载等。


📌 容器如何实现资源隔离

  • 容器通过将资源(如网络接口、进程 ID 等)分配到不同的命名空间来实现资源的隔离。
  • 例如,网络命名空间会将网络接口列表存储在 struct net 中,而不是使用全局的网络接口列表。

📊 默认命名空间与创建新命名空间

  • 系统启动时会初始化一个默认的命名空间(如 init_net),所有进程默认共享该命名空间。
  • 当创建新的命名空间时,系统会为新命名空间创建一个新的 net 命名空间,并将新进程关联到这个新的命名空间,而不是默认的命名空间。

访问当前进程(Accessing the Current Process)

在这里插入图片描述

访问当前进程是一个常见的操作,很多系统调用都需要访问当前进程的相关信息:

  • 打开文件时需要访问 struct\ task_struct 中的 file 字段。
  • 映射新文件时需要访问 struct\ task_struct 中的 mm 字段。

📌 访问当前进程的宏

为了支持快速访问当前进程,尤其是在多处理器配置中,每个 CPU 都有一个变量来存储和获取指向当前 struct\ task_struct 的指针。

以前的实现方式

在以前,current 宏的实现方式如下:

/* 获取当前栈指针 */
register unsigned long current_stack_pointer asm("esp") __attribute_used__;

/* 获取当前线程信息结构体 */
static inline struct thread_info *current_thread_info(void)
{
   return (struct thread_info *)(current_stack_pointer & ~(THREAD_SIZE – 1));
}

#define current current_thread_info()->task
  • s t r u c t   t a s k _ s t r u c t struct\ task\_struct struct task_struct:Linux 内核中用于表示进程(包括线程)的核心数据结构,包含了进程的调度信息、文件描述符、地址空间等。

  • c u r r e n t current current 宏:用于快速访问当前进程的 t a s k _ s t r u c t task\_struct task_struct 结构体。在多核处理器上,它通过每个 CPU 特有的变量来加速访问。

  • c u r r e n t _ s t a c k _ p o i n t e r current\_stack\_pointer current_stack_pointer:通过汇编指令直接获取当前栈指针,用于定位当前线程的信息。

  • t h r e a d _ i n f o thread\_info thread_info:存储与线程相关的信息,通常包含线程的状态、栈、进程 ID 等。通过 c u r r e n t _ s t a c k _ p o i n t e r current\_stack\_pointer current_stack_pointer`获取线程信息后,可以进一步访问线程的任务结构体。

  • THREAD_SIZE:定义了线程的栈大小,通常用于按位操作栈指针,确定当前线程的信息。

上下文切换(Context Switching)

在这里插入图片描述
上图展示了 Linux 内核中上下文切换的整体流程:

  1. 触发内核态:上下文切换只能在内核态进行,通常由系统调用或中断引发。此时,用户空间寄存器会被保存到内核栈中。
  2. 调用 schedule():在某些时刻(如线程阻塞等待 I/O 或时间片用尽),内核会调用 schedule(),决定从线程 T0 切换到线程 T1。
  3. context_switch() 函数:该函数执行与架构相关的操作,如更新地址空间(若从用户态切换到其他用户态)并维护 TLB(Translation Lookaside Buffer)。
    • !next->mm(目标切换到内核线程),则共享 prev->active_mm
    • next->mm(目标切换到用户进程),则调用 switch_mm_irqs_off() 处理内存映射切换。
  4. switch_to()context_switch() 最终调用 switch_to()进行真正的寄存器状态和内核栈切换。其实现包含以下关键步骤:
    • 保存被调度出线程的寄存器到当前栈:如 ebp, ebx, edi, esi 等“调用者保存的寄存器”;
    • 更新 esp(栈指针),切换到新线程的内核栈;
    • 恢复新线程的寄存器,然后跳转到 __switch_to 继续执行。

首先要注意的是,上下文切换之前必须完成一次从用户空间到内核空间的过渡(如通过系统调用或中断)。在这一点上,用户空间的寄存器状态会被保存到内核栈上。当调度器调用 schedule() 函数后,可能判定当前线程(如 T0)需要切换到另一个线程(如 T1),原因可能包括当前线程等待I/O操作完成而阻塞,或其时间片耗尽。

此时,context_switch() 会执行特定于体系结构的操作,并在需要时切换地址空间:

static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
               struct task_struct *next, struct rq_flags *rf)
{
    prepare_task_switch(rq, prev, next);
    arch_start_context_switch(prev);

    if (!next->mm) { // 若目标线程为内核线程(无用户地址空间)
        enter_lazy_tlb(prev->active_mm, next);
        next->active_mm = prev->active_mm;
        if (prev->mm)
            mmgrab(prev->active_mm);
        else
            prev->active_mm = NULL;
    } else { // 若目标线程为用户线程
        membarrier_switch_mm(rq, prev->active_mm, next->mm);
        switch_mm_irqs_off(prev->active_mm, next->mm, next);
        if (!prev->mm) { // 如果来源线程是内核线程
            rq->prev_mm = prev->active_mm;
            prev->active_mm = NULL;
        }
    }

    rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);
    prepare_lock_switch(rq, next, rf);
    switch_to(prev, next, prev); // 执行寄存器状态和内核栈切换
    barrier();

    return finish_task_switch(prev);
}

接下来调用特定于体系结构的函数 s w i t c h _ t o switch\_to switch_to,它负责切换寄存器状态和内核栈指针。寄存器状态保存在栈中,而栈指针被保存在任务结构体中:

#define switch_to(prev, next, last)               \
do {                                              \
    ((last) = __switch_to_asm((prev), (next)));   \
} while (0)

下面是 x86 架构上 _ _ s w i t c h _ t o _ a s m \_\_switch\_to\_asm __switch_to_asm 的汇编实现:

SYM_CODE_START(__switch_to_asm)
    pushl   %ebp
    pushl   %ebx
    pushl   %edi
    pushl   %esi
    pushfl

    movl    %esp, TASK_threadsp(%eax)
    movl    TASK_threadsp(%edx), %esp

#ifdef CONFIG_STACKPROTECTOR
    movl    TASK_stack_canary(%edx), %ebx
    movl    %ebx, PER_CPU_VAR(stack_canary)+stack_canary_offset
#endif

#ifdef CONFIG_RETPOLINE
    FILL_RETURN_BUFFER %ebx, RSB_CLEAR_LOOPS, X86_FEATURE_RSB_CTXSW
#endif

    popfl
    popl    %esi
    popl    %edi
    popl    %ebx
    popl    %ebp

    jmp     __switch_to
SYM_CODE_END(__switch_to_asm)

在上下文切换过程中,你会发现指令指针(IP / EIP / RIP)并没有被显式保存。之所以不需要额外保存,有以下几个原因:

  1. 总是在同一个函数中恢复执行
    当任务恢复时,它会回到同一个函数内继续执行,从而不必担心额外的指令指针保存。

  2. schedule()(或 context_switch() 内联)的调用者返回地址已保存在内核栈中
    schedule() 所在函数(或直接调用 context_switch() 的地方)的返回地址会保存在栈中。当切换回来时,CPU 会自动从栈中取出这个返回地址。

  3. 使用 jmp 执行 __switch_to() 函数
    __switch_to() 函数执行完毕并返回时,它会从栈中弹出原先(下一个任务)的返回地址,因而无需我们显式地保存 IP。


🧠 关键概念解释

  • 指令指针(Instruction Pointer)
    指示 CPU 正在执行的下一条指令的地址。在 x86 上通常被称为 EIP(32 位)或 RIP(64 位)。

  • 内核栈(Kernel Stack)
    每个任务都有自己的内核栈,用于在内核态保存函数调用、寄存器以及返回地址等信息。

  • schedule()context_switch()
    Linux 内核调度的重要函数,它们在切换任务时会内联或调用一系列的汇编/内核逻辑来完成上下文切换。

阻塞与唤醒任务(Blocking and Waking Up Tasks)

任务状态(Task States)

下图展示了任务(线程)的各种状态以及它们之间的可能转换路径:
在这里插入图片描述

  • clone() 创建新任务,初始状态为 TASK_READY
  • 当调度器调用 schedule() 后,任务变为 TASK_RUNNING
  • 如果任务调用 exit(),它会变为 TASK_DEADTASK_ZOMBIE
  • 运行中的任务可以因为 I/O 或其他事件调用 wait_event() 进入阻塞状态:
    • TASK_INTERRUPTIBLE:可中断的睡眠状态,可以被信号唤醒;
    • TASK_UNINTERRUPTIBLE:不可中断的睡眠状态,只有事件发生才唤醒。
  • 被阻塞的任务可通过 wake_up() 被重新唤醒并返回 TASK_READY 状态,等待再次被调度执行。

阻塞当前线程(Blocking the Current Thread)

阻塞当前线程是操作系统中一个关键操作,它的意义在于当当前线程无法继续运行(例如等待 I/O 完成)时,让出 CPU,让其他线程得以运行,从而实现高效调度。

实现线程阻塞的操作步骤如下:

  1. 将当前线程的状态设置为 TASK_UNINTERRUPTIBLETASK_INTERRUPTIBLE
  2. 将该线程添加到等待队列中;
  3. 调用调度器 schedule(),选择另一个 READY 队列中的任务;
  4. 执行上下文切换,将 CPU 控制权交给新的任务。

等待队列是一个链表,每个元素包含一些额外的信息,如指向任务结构体的指针。

💡 关于 wait_event 的重要说明

  • wait_eventwake_up 之间的时序非常关键。为了防止死锁,在检查条件前,任务必须先加入等待队列
  • 唤醒操作 wake_up 会检查条件是否满足,并在满足时唤醒线程;
  • 唤醒前还会检查是否有信号(如果线程为可中断状态);
  • 唤醒后,调度器会重新安排任务执行,通过 schedule()完成实际的上下文切换。

  • TASK_RUNNING:线程正在 CPU 上执行;
  • TASK_READY:线程就绪,等待调度;
  • TASK_INTERRUPTIBLE:线程在等待某事件,可被信号中断;
  • TASK_UNINTERRUPTIBLE:线程在等待某事件,不能被信号中断;
  • wait_event:内核机制,用于让线程等待某个条件;
  • wake_up:当条件满足时,唤醒被阻塞的线程;
  • schedule():内核调度函数,选择下一个要运行的线程。

唤醒任务(Waking up a Task)

我们可以使用内核提供的 wake_up 原语来唤醒被阻塞的任务。当某个事件(如 I/O 完成)发生时,内核会调用 wake_up 将等待中的线程重新置为可运行状态。其主要操作流程如下:


🧱 唤醒流程(高层操作步骤)

  1. 从等待队列中选择一个任务
    找到在特定等待队列中阻塞的任务,通常这些任务正在等待某个条件或事件完成。
  2. 将任务状态设置为 TASK_READY
    这意味着该任务已经可以继续执行,不再处于 TASK_INTERRUPTIBLETASK_UNINTERRUPTIBLE 阻塞状态。
  3. 将任务插入调度器的就绪队列(READY queue)中
    被唤醒的任务将重新加入调度器的就绪队列,等待系统调度器下一次调度。

🧠 多核系统中的复杂性(SMP 系统)

在 SMP(对称多处理)系统中,唤醒任务的过程更为复杂,原因如下:

  • 每个 CPU 核心有自己的就绪队列
    为了避免锁竞争与提升并行性,Linux 在每个 CPU 上维护独立的调度队列。
  • 任务队列之间需要负载均衡
    如果唤醒的任务在不同的 CPU 上被调度,系统需要确保多个队列负载平衡,防止有的 CPU 空闲而有的 CPU 拥堵。
  • 唤醒其他 CPU 可能需要发信号
    如果目标任务位于非当前 CPU 的就绪队列中,需要通过 IPI(中断)唤醒对应的处理器,以便它能及时调度该任务。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【开源项目】Excel手撕AI算法深入理解(四):AlphaFold、Autoencoder

项目源码地址:https://github.com/ImagineAILab/ai-by-hand-excel.git 一、AlphaFold AlphaFold 是 DeepMind 开发的突破性 AI 算法,用于预测蛋白质的三维结构。它的出现解决了生物学领域长达 50 年的“蛋白质折叠问题”,被《科学》杂志评为…

第IV部分有效应用程序的设计模式

第IV部分有效应用程序的设计模式 第IV部分有效应用程序的设计模式第23章:应用程序用户界面的架构设计23.1设计考量23.2示例1:用于非分布式有界上下文的一个基于HTMLAF的、服务器端的UI23.3示例2:用于分布式有界上下文的一个基于数据API的客户端UI23.4要点第24章:CQRS:一种…

如何编制实施项目管理章程

本文档概述了一个项目管理系统的实施计划,旨在通过统一的业务规范和技术架构,加强集团公司的业务管控,并规范业务管理。系统建设将遵循集团统一模板,确保各单位项目系统建设的标准化和一致性。 实施范围涵盖投资管理、立项管理、设计管理、进度管理等多个方面,支持项目全生…

排序(java)

一.概念 排序:对一组数据进行从小到大/从大到小的排序 稳定性:即使进行排序相对位置也不受影响如: 如果再排序后 L 在 i 的前面则稳定性差,像图中这样就是稳定性好。 二.常见的排序 三.常见算法的实现 1.插入排序 1.1 直…

【HDFS入门】HDFS副本策略:深入浅出副本机制

目录 1 HDFS副本机制概述 2 HDFS副本放置策略 3 副本策略的优势 4 副本因子配置 5 副本管理流程 6 最佳实践与调优 7 总结 1 HDFS副本机制概述 Hadoop分布式文件系统(HDFS)的核心设计原则之一就是通过数据冗余来保证可靠性,而这一功能正是通过副本策略实现的…

智能 GitHub Copilot 副驾驶® 更新升级!

智能 GitHub Copilot 副驾驶 迎来重大升级!现在,所有 VS Code 用户都能体验支持 Multi-Context Protocol(MCP)的全新 Agent Mode。此外,微软还推出了智能 GitHub Copilot 副驾驶 Pro 订阅计划,提供更强大的…

【今日三题】添加字符(暴力枚举) / 数组变换(位运算) / 装箱问题(01背包)

⭐️个人主页:小羊 ⭐️所属专栏:每日两三题 很荣幸您能阅读我的文章,诚请评论指点,欢迎欢迎 ~ 目录 添加字符(暴力枚举)数组变换(位运算)装箱问题(01背包) 添加字符(暴力枚举) 添加字符 当在A的开头或结尾添加字符直到和B长度…

Linux——消息队列

目录 一、消息队列的定义 二、相关函数 2.1 msgget 函数 2.2 msgsnd 函数 2.3 msgrcv 函数 2.4 msgctl 函数 三、消息队列的操作 3.1 创建消息队列 3.2 获取消息队列并发送消息 3.3 从消息队列接收消息recv 四、 删除消息队列 4.1 ipcrm 4.2 msgctl函数 一、消息…

领慧立芯LHE7909可兼容替代TI的ADS1299

LHE7909是一款由领慧立芯(Legendsemi)推出的24位高精度Δ-Σ模数转换器(ADC),主要面向医疗电子和生物电势测量应用,如脑电图(EEG)、心电图(ECG)等设备。以下是…

MongoDB简单用法

图片中 MongoDB Compass 中显示了默认的三个数据库: adminconfiglocal 如果在 .env 文件中配置的是: MONGODB_URImongodb://admin:passwordlocalhost:27017/ MONGODB_NAMERAGSAAS💡 一、为什么 Compass 里没有 RAGSAAS 数据库?…

uniapp-商城-26-vuex 使用流程

为了能在所有的页面都实现状态管理,我们按照前面讲的页面进行状态获取,然后再进行页面设置和布局,那就是重复工作,vuex 就会解决这样的问题,如同类、高度提炼的接口来帮助我们实现这些重复工作的管理。避免一直在造一样的轮子。 https://vuex.vuejs.org/zh/#%E4%BB%80%E4…

UDP概念特点+编程流程

UDP概念编程流程 目录 一、UDP基本概念 1.1 概念 1.2 特点 1.2.1 无连接性: 1.2.2 不可靠性 1.2.3 面向报文 二、UDP编程流程 2.1 客户端 cli.c 2.2 服务端ser.c 一、UDP基本概念 1.1 概念 UDP 即用户数据报协议(User Datagram Protocol &…

Flutter项目之设置页

目录: 1、实现效果图2、实现流程2.1、引入依赖2.2、封装弹窗工具类2.3、设置页2.4、路由中注册设置页面 1、实现效果图 2、实现流程 2.1、引入依赖 2.2、封装弹窗工具类 import package:fluttertoast/fluttertoast.dart;class CommontToast {static showToast(Str…

通过GO后端项目实践理解DDD架构

最近在工作过程中重构的项目要求使用DDD架构,在网上查询资料发现教程五花八门,并且大部分内容都是长篇的概念讲解,晦涩难懂,笔者看了一些github上入门的使用DDD的GO项目,并结合自己开发中的经验,谈谈自己对…

天线静电防护:NRESDTLC5V0D8B

一. 物联网天线的使用环境 1.1 联网天线广泛应用于智能家居领域,比如智能门锁、智能摄像头等设备中,通过天线实现设备与家庭网络的连接,用户可以远程控制和监控家居设备。以智能摄像头为例,它通过天线将拍摄的画面实时传输到用户…

【Linux 并发与竞争】

【Linux 并发与竞争】 Linux是一个多任务操作系统,肯定会存在多个任务共同操作同一段内存或者设备的情况,多个任务甚至中断都能访问的资源叫做共享资源,就和共享单车一样。在驱动开发中要注意对共享资源的保护,也就是要处理对共享…

实用类题目

1. 密码强度检测 题目描述:生活中,为保证账户安全,密码需要有一定强度。编写一个方法,接收一个字符串作为密码,判断其是否符合以下强度要求:长度至少为 8 位,包含至少一个大写字母、一个小写字…

STM32F103C8T6-基于FreeRTOS系统实现步进电机控制

引言 上一篇文章讲述了如何使用蓝牙连接stm32进行数据收发控制步进电机,这篇在之前的基础上通过移植操作系统(FreeRTOS或者其他的也可以,原理操作都类似)实现步进电机控制。 上篇博客指路:STM32蓝牙连接Android实现云…

macOS安装java

一、下载 官网Java Downloads | Oracle 安装载java8,下载对应的JDK Java Downloads | Oracle 二、双击安装 安装 完成 三、查看安装位置 打开终端窗口,执行命令: /usr/libexec/java_home -V /Library/Java/JavaVirtualMachines/jdk-1.8.jdk/Content…

zkmall模块商城:B2C 场景下 Vue3 前端性能优化的广度探索与实践

ZKmall作为面向B2C场景的模块化电商平台,其前端性能优化在Vue3框架下的实践融合了架构设计、渲染机制与业务特性,形成了一套多维度的优化体系。以下从技术实现与业务适配两个维度展开分析: 一、Vue3响应式系统深度适配 ​Proxy驱动的精准更新…