多任务功能
多任务处理 |
---|
进程 |
线程或任务 |
同步 |
调度程序 |
中断核心 |
CPU 特定 |
Linux 内核是一个抢占式 多任务 操作系统。作为一个多任务操作系统,它允许多个进程共享处理器 (CPU) 和其他系统资源。每个 CPU 每次执行一个任务。但是,多任务处理允许每个处理器在执行的任务之间切换,而无需等待每个任务完成。为此,内核可以在任何时间暂时中断处理器正在执行的任务,并用另一个任务替换它,该任务可以是新的任务,也可以是先前挂起的任务。涉及运行任务交换的操作称为上下文切换。
进程是运行中的用户空间程序。内核在函数 run_init_process id 中使用 kernel_execve id 启动第一个进程 /sbin/init。进程占用系统资源,例如内存、CPU 时间。系统调用 sys_fork id 和 sys_execve id 用于从用户空间创建新进程。进程使用 sys_exit id 系统调用退出。
Linux 从 Unix 继承了其基本进程管理系统调用 (⚲ API ↪ ⚙️ 实现)
man 2 fork ↪ kernel_clone id 通过 复制 调用它的进程来创建一个新进程。
man 2 _exit ↪ do_exit id “立即”终止调用进程。属于该进程的任何打开的文件描述符都将关闭。
man 2 wait ↪ kernel_waitid id 挂起调用进程的执行,直到其子进程之一终止。
man 2 execve ↪ do_execve id 在当前进程的上下文中运行可执行文件,替换先前的可执行文件。此系统调用由 libc man 3 exec 函数族使用。
Linux 使用其自己的系统调用 man 2 clone 来增强传统的 Unix 进程 API。克隆创建一个子进程,该子进程可以与父进程共享其执行上下文的一部分。它通常用于实现线程(尽管程序员通常会使用更高级别的接口,例如 man 7 pthreads,它是在克隆之上实现的)。
PID - 进程标识符 定义为 pid_t id 是唯一的顺序编号。 man 1 ps -A 列出当前进程。系统调用 man 2 getpid ↪ task_tgid_vnr id 返回当前进程的 PID,在内部称为 TGID - 线程组 ID。一个进程可以包含多个线程。 man 2 gettid ↪ task_pid_vnr id 返回线程 ID。在内部历史上称为 PID。⚠️ 警告:混淆。用户空间 PID ≠ 内核空间 PID。 man 1 ps -AF 列出当前进程和线程作为 LWP。对于单线程进程,所有这些 ID 都是相等的。
⚲ API
⚙️ 内部
- task_structid
- pid_typeid
- kernel/fork.csrc
- syscalls
- man 2 set_tid_address – 设置指向线程 ID 的指针
- man 2 fork – 创建子进程
- man 2 vfork – 创建子进程并阻塞父进程
- man 2 clone – 创建子进程
- man 2 unshare – 取消关联进程执行上下文的部分
- kernel/sys.csrc
- syscalls
- man 2 prctl – 对进程或线程的操作
- kernel/pid.csrc
- syscalls
- man 2 pidfd_open – 获取指向进程的文件描述符
- man 2 pidfd_getfd – 获取另一个进程的文件描述符的副本
- syscalls
- man 2 pidfd_open – 获取指向进程的文件描述符
- man 2 pidfd_getfd – 获取另一个进程的文件描述符的副本
- kernel/exit.csrc
- syscalls
- man 2 exit – 终止调用进程
- man 2 exit_group – 退出进程中的所有线程
- man 2 waitid – 等待进程状态改变
- man 2 waitpid – 等待进程状态改变
📖 参考资料
进程间通信
[edit | edit source]进程间通信 (IPC) 特指操作系统提供的机制,用于允许其管理的进程共享数据。实现 IPC 的方法分为几类,它们根据软件需求(如性能和模块化需求)以及系统环境而有所不同。Linux 从 Unix 继承了以下 IPC 机制
信号 (⚲ API ↪ ⚙️ 实现)
- man 2 kill 向进程发送信号
- man 2 tgkill ↪ do_tkill id 向线程发送信号
- man 2 process_vm_readv ↪ process_vm_rw id - 进程地址空间之间零拷贝数据传输
🔧 TODO: man 2 sigaction man 2 signal man 2 sigaltstack man 2 sigpending man 2 sigprocmask man 2 sigsuspend man 2 sigwaitinfo man 2 sigtimedwait
- 匿名管道 和命名管道 (FIFO) man 2 mknod ↪ do_mknodat id S_IFIFO id
- 快速数据路径 PF_XDP id
- Unix 域套接字 PF_UNIX id
- 内存映射文件 man 2 mmap ⤑ ksys_mmap_pgoff id
- Sys V IPC
- 消息队列
- 信号量
- 共享内存: man 2 shmget, man 2 shmctl, man 2 shmat, man 2 shmdt
📖 参考资料
线程或任务
[edit | edit source]在 Linux 内核中,“线程”和“任务”几乎是同义词。
💾 历史:直到 2.6.39,内核模式只有一个线程,由 大内核锁 保护。
⚲ API
- linux/sched.h inc - 主要调度程序 API
- arch/x86/include/asm/current.hsrc
- current id 和 get_current id () 返回当前 task_struct id
- uapi/linux/taskstats.h inc 每个任务的统计信息
- linux/thread_info.hinc
- 函数 current_thread_info id() 返回 thread_info id
- linux/sched/task.h inc - 调度程序和各种任务生命周期 (fork()/exit()) 功能之间的接口
- linux/kthread.h inc - 用于创建和停止内核线程的简单接口,无需麻烦。
- kthread_run id 创建并唤醒线程
- kthread_createid
⚙️ 内部
调度程序
[edit | edit source]调度程序 是操作系统中决定在特定时间点运行哪个进程的部分。它通常能够暂停正在运行的进程,将其移到运行队列的末尾,并启动一个新进程。
活动进程被放置在一个称为运行队列 的数组中,或运行队列 - rq id。运行队列可能包含每个进程的优先级值,调度程序将使用这些值来确定下一个运行哪个进程。为了确保每个程序都能公平地共享资源,每个程序都会运行一段时间(时间片),然后暂停并放回运行队列中。当一个程序停止让另一个程序运行时,运行队列中优先级最高的程序将被允许执行。当进程请求睡眠、等待资源可用或被终止时,它们也会从运行队列中移除。
Linux 使用 完全公平调度程序 (CFS),这是第一个在通用操作系统中广泛使用的公平队列进程调度程序的实现。CFS 使用一种经过充分研究的经典调度算法,称为“公平队列”,最初是为分组网络发明的。CFS 调度程序的调度复杂度为 O(log N),其中 N 是运行队列中的任务数。选择任务可以在恒定时间内完成,但是任务运行后重新插入需要 O(log N) 操作,因为运行队列是用 红黑树 实现的。
与之前的 O(1) 调度程序 相比,CFS 调度程序的实现不是基于运行队列。相反,红黑树实现了一个“时间线”,用于描述未来任务的执行。此外,调度程序使用纳秒级粒度记账,这是分配给单个进程的 CPU 使用份额的原子单位(因此使得之前的时间片概念变得多余)。这种精确的知识也意味着不需要特定的启发式方法来确定进程的交互性,例如。
与旧的 O(1) 调度程序一样,CFS 使用一个称为“睡眠公平”的概念,它将睡眠或等待的任务视为与运行队列中的任务等效。这意味着,当交互式任务需要 CPU 时间时,它们可以获得与在用户输入或其他事件等待期间花费的大部分时间相似的 CPU 时间份额。
用于调度算法的数据结构是红黑树,其中节点是特定于调度程序的结构,称为 sched_entity id。 这些是从通用task_struct进程描述符派生的,并添加了调度程序元素。 这些节点按纳秒级的处理器执行时间索引。 每个进程还计算出最大执行时间。 此时间基于这样一种想法,即“理想处理器”将在所有进程之间平等地共享处理能力。 因此,最大执行时间是进程等待运行的时间除以进程总数,或者换句话说,最大执行时间是进程在“理想处理器”上预期运行的时间。
当调用调度程序来运行新进程时,调度程序的操作如下:
- 选择调度树的最左节点(因为它将具有最低的已消耗执行时间),并将其发送以执行。
- 如果进程简单地完成执行,它将从系统和调度树中删除。
- 如果进程达到其最大执行时间或以其他方式停止(自愿或通过中断),则它将根据其新的已消耗执行时间重新插入到调度树中。
- 然后将从树中选择新的最左节点,重复迭代。
如果进程将其大部分时间花费在休眠上,则其已消耗时间值很低,并且当它最终需要时,它会自动获得优先级提升。 因此,此类任务不会比不断运行的任务获得更少的处理器时间。
CFS 的替代方法是 Con Kolivas 创建的 Brain Fuck Scheduler (BFS)。 与其他调度程序相比,BFS 的目标是提供一个具有更简单算法的调度程序,该算法不需要调整启发式算法或调整参数来使性能适应特定类型的计算工作负载。
Con Kolivas 还维护着 CFS 的另一个替代方案,即 MuQSS 调度程序。[1]
Linux 内核包含不同的调度程序类(或策略)。 目前默认使用的完全公平调度程序是 SCHED_NORMAL id 调度程序类,也称为 SCHED_OTHER。 内核还包含另外两个类 SCHED_BATCH id 和 SCHED_IDLE id,以及另外两个实时调度程序类,名为 SCHED_FIFO id(实时先入先出)和 SCHED_RR id(实时循环调度),以及第三个实时调度策略,称为 SCHED_DEADLINE id,它实现了 最早截止期限优先算法 (EDF),该算法稍后添加。 任何实时调度程序类都优先于任何“正常”(即非实时)类。 调度程序类是通过 man 2 sched_setscheduler ↪ do_sched_setscheduler id 系统调用选择和配置的。
在调度程序中适当地平衡延迟、吞吐量和公平性是一个开放性问题。[1]
⚲ API
- man 1 renice – 运行进程的优先级
- man 1 nice – 以修改后的调度优先级运行程序
- man 1 chrt – 操作进程的实时属性
- man 2 sched_getattr ↪ sys_sched_getattr id – 获取调度策略和属性
- linux/sched.h inc – 主要调度程序 API
- man 2 getpriority,man 2 setpriority
- man 2 sched_setscheduler,man 2 sched_getscheduler
⚙️ 内部
- sched_init id 从 start_kernel id 调用
- __schedule id 是主要的调度程序函数。
- runqueues id,this_rq id
- kernel/schedsrc
- kernel/sched/core.csrc
- kernel/sched/fair.c src 实现 SCHED_NORMAL id,SCHED_BATCH id,SCHED_IDLE id
- sched_setscheduler id,sched_getscheduler id
- task_struct id::rt_priority id 以及其他具有不太独特标识符的成员
🛠️ 工具
📖 参考资料
- man 7 sched
- 调度 doc
- CFS
- 完全公平调度程序 LWN
- 截止日期任务调度程序 doc
- sched ltp
- sched_setparam ltp
- sched_getscheduler ltp
- sched_setscheduler ltp
📚 关于调度程序的进一步阅读
抢占
[edit | edit source]抢占是指系统中断正在运行的任务以切换到另一个任务的能力。 这是确保高优先级任务获得必要的 CPU 时间并提高系统响应能力所必需的。 在 Linux 中,抢占模型定义了内核如何以及何时抢占任务。 不同的模型在系统响应能力和吞吐量之间提供了不同的权衡。
📖 参考资料
- kernel/Kconfig.preemptsrc
- CONFIG_PREEMPT_NONE id – 服务器没有强制抢占
- CONFIG_PREEMPT_VOLUNTARY id – 桌面自愿抢占
- CONFIG_PREEMPT id – 除关键部分外,低延迟桌面可抢占
- CONFIG_PREEMPT_RT id – 实时抢占,用于 高度响应的应用程序
- CONFIG_PREEMPT_DYNAMIC id,请参见 /sys/kernel/debug/sched/preempt
等待队列
[edit | edit source]内核中的等待队列是一种数据结构,它允许一个或多个进程等待(休眠),直到发生感兴趣的事情。它们在整个内核中被用来等待可用内存、I/O 完成、消息到达以及许多其他事情。在 Linux 的早期,等待队列只是一个简单的等待进程列表,但各种可扩展性问题(包括惊群问题)导致从那时起添加了相当多的复杂性。
⚲ API
wait_queue_head id 由 wait_queue_entry id 的双向链表和一个自旋锁组成。
等待简单事件
- 使用两种方法之一来初始化 wait_queue_head id
- init_waitqueue_head id 在函数上下文中初始化 wait_queue_head id
- DECLARE_WAIT_QUEUE_HEAD id - 实际上在全局上下文中定义 wait_queue_head id
- 等待替代方案
- wait_event_interruptible id - 首选等待
- wait_event_interruptible_timeoutid
- wait_event id - 不可中断等待。可能导致死锁 ⚠
- wake_up id 等
👁 例如,请参阅对唯一的 suspend_queue id 的引用。
在复杂情况下,显式使用 add_wait_queue 而不是简单的 wait_event
- DECLARE_WAITQUEUE id 实际上使用 default_wake_function id 定义 wait_queue_entry
- add_wait_queue id 将进程插入等待队列的第一个位置
- remove_wait_queueid
⚙️ 内部
📖 参考资料
同步
[edit | edit source]线程同步被定义为一种机制,它确保两个或多个并发进程或线程不会同时执行某个特定的程序段,称为互斥(互斥)。当一个线程开始执行临界区(程序的序列化段)时,另一个线程应该等待,直到第一个线程完成。如果没有应用适当的同步技术,它可能会导致竞争条件,其中变量的值可能是不可预测的,并且会根据进程或线程的上下文切换时间而变化。
用户空间同步
[edit | edit source]Futex
[edit | edit source]一个 man 2 futex ↪ do_futex id(“快速用户空间互斥体”的缩写)是一个内核系统调用,程序员可以使用它来实现基本锁定,或作为更高级别锁定抽象的构建块,例如信号量和 POSIX 互斥体或条件变量。
Futex 由一个内核空间等待队列组成,该队列附加到用户空间中的对齐整数。多个进程或线程完全在用户空间中操作该整数(使用原子操作以避免相互干扰),并且仅在请求等待队列上的操作(例如唤醒等待进程或将当前进程放入等待队列)时才诉诸于相对昂贵的系统调用。经过正确编程的基于 futex 的锁将不会使用系统调用,除非锁存在争用;由于大多数操作不需要进程之间的仲裁,因此在大多数情况下不会发生这种情况。
futex 的基本操作仅基于两个核心操作 futex_wait id 和 futex_wake id,尽管实现对于更多专门情况有更多操作。
- WAIT(addr,val)检查存储在地址addr处的 value 是否为val,如果是,则将当前线程置于休眠状态。
- WAKE(addr,val)唤醒等待地址addr的val个线程。
⚲ API
⚙️ 内部结构:kernel/futex.c src
📖 参考资料
文件锁定
[edit | edit source]⚲ API:man 2 flock
信号量
[edit | edit source]💾 历史:信号量是 System V IPC man 7 sysvipc 的一部分
⚲ API
⚙️ 内部结构:ipc/sem.c src
内核空间同步
[edit | edit source]对于内核模式同步,Linux 提供了三类锁定原语:休眠、每个 CPU 本地锁和自旋锁。
休眠锁
[edit | edit source]读-复制-更新
[edit | edit source]解决读者-写者问题的常用机制是读-复制-更新(RCU)算法。读-复制-更新实现了一种对读者无等待(非阻塞)的互斥,允许非常低的开销。但是,RCU 更新可能很昂贵,因为它们必须将数据结构的旧版本保留在适当的位置,以适应现有的读者。
💾 历史:RCU 于 2002 年 10 月添加到 Linux。从那时起,内核中存在着数千种 RCU API 的使用,包括网络协议栈和内存管理系统。Linux 内核 2.6 版本中 RCU 的实现是最知名的 RCU 实现之一。
⚲ linux/rcupdate.h inc 中的核心 API 很小
- rcu_read_lock id 标记受 RCU 保护的数据结构,以便在该临界区持续期间不会回收它。
- rcu_read_unlock id 用于读者告知回收器读者已退出 RCU 读取端临界区。请注意,RCU 读取端临界区可以嵌套和/或重叠。
- synchronize_rcu id 会阻塞,直到所有 CPU 上所有现有的 RCU 读取端临界区都已完成。请注意,
synchronize_rcu
不会必然等待任何后续的 RCU 读取端临界区完成。
👁 例如,考虑以下事件序列
CPU 0 CPU 1 CPU 2 ----------------- ------------------------- --------------- 1. rcu_read_lock() 2. enters synchronize_rcu() 3. rcu_read_lock() 4. rcu_read_unlock() 5. exits synchronize_rcu() 6. rcu_read_unlock()
- 由于
synchronize_rcu
是必须弄清楚读者何时完成的 API,因此它的实现是 RCU 的关键。为了使 RCU 在除最密集读取的情况之外的所有情况下都变得有用,synchronize_rcu
的开销也必须非常小。
- 或者,同步_rcu 可以注册一个回调函数,在所有正在进行的 RCU 读取侧关键区段完成后调用,而不是阻塞。这种回调变体在 Linux 内核中被称为 call_rcu id。
- rcu_assign_pointer id - 更新器使用此函数将新值分配给由 RCU 保护的指针,以便安全地将值的更改从更新器传达给读取器。此函数返回新值,并执行给定 CPU 架构所需的任何 内存屏障 指令。也许更重要的是,它用于记录哪些指针受 RCU 保护。
- rcu_dereference id - 读取器使用此函数获取由 RCU 保护的指针,它返回一个值,然后可以安全地取消引用。它还执行编译器或 CPU 所需的任何指令,例如,gcc 的易失性强制转换、C/C++11 的 memory_order_consume 加载或旧 DEC Alpha CPU 所需的内存屏障指令。
rcu_dereference
返回的值仅在包含的 RCU 读取侧关键区段内有效。与rcu_assign_pointer
一样,rcu_dereference
的一个重要功能是记录哪些指针受 RCU 保护。
RCU 基础设施观察 rcu_read_lock
、rcu_read_unlock
、synchronize_rcu
和 call_rcu
调用的时间顺序,以便确定何时 (1) synchronize_rcu
调用可以返回给调用者,以及 (2) call_rcu
回调可以被调用。RCU 基础设施的高效实现大量使用批处理,以便将开销分摊到相应 API 的多次使用上。
⚙️ 内部
📖 参考资料
互斥量
[edit | edit source]⚲ API
- linux/mutex.hinc
- linux/completion.hinc
- mutex id 拥有者和使用约束,比信号量更容易调试
- rt_mutex id 带有优先级继承 (PI) 支持的阻塞互斥锁
- ww_mutex id 伤口/等待互斥量:带有死锁避免功能的阻塞互斥锁
- rw_semaphore id 读写信号量
- percpu_rw_semaphoreid
- completion id - 使用完成来同步具有 ISR 和任务或两个任务的任务。
💾 历史
- semaphore id - 如果可能,使用互斥量代替信号量
- linux/semaphore.hinc
- linux/rwsem.hinc
📖 参考资料
每个 CPU 本地锁
[edit | edit source]
在正常的可抢占内核中,local_lock 调用 preempt_disable id。在 RT 可抢占内核中,local_lock 调用 migrate_disable id 和 spin_lock id。
⚲ API
📖 参考资料
💾 历史:在内核版本 2.6 之前,Linux 禁用中断来实现短关键区段。从 2.6 及更高版本开始,Linux 是完全可抢占的。
自旋锁
[edit | edit source]自旋锁
[edit | edit source]自旋锁是一种锁,它会导致试图获取它的线程简单地在循环中等待 (“自旋”),同时反复检查锁是否可用。由于线程保持活动状态但没有执行有用的任务,因此使用这种锁是一种忙等待。一旦获取,自旋锁通常会一直保持,直到被显式释放,尽管在某些实现中,如果等待的线程 (持有锁的线程) 阻塞或“进入睡眠”,它们可能会被自动释放。
自旋锁通常在内核中使用,因为如果线程可能只被阻塞很短时间,它们效率很高。但是,如果自旋锁保持较长时间,它们就会变得浪费,因为它们可能会阻止其他线程运行并需要重新调度。👁 例如 kobj_kset_join id 使用自旋锁来保护对链表的访问。
内核抢占的启用和禁用取代了单处理器系统上的自旋锁 (禁用 CONFIG_SMP id)。大多数自旋锁在 CONFIG_PREEMPT_RT id 内核中成为睡眠锁。
📖 参考资料
顺序锁
[edit | edit source]顺序锁 (顺序锁的简称) 是一种特殊的锁定机制,用于在 Linux 中支持两个并行操作系统例程之间共享变量的快速写入。当写入器数量很少时,它是读者-写入器问题的特殊解决方案。
它是一种读写一致的机制,可以避免写入器饥饿问题。一个 seqlock_t id 包含用于保存序列计数器 seqcount_t id/seqcount_spinlock_t 的存储,以及一个锁。该锁用于支持两个写入器之间的同步,计数器用于指示读取器的一致性。除了更新共享数据外,写入器还会在获取锁后和释放锁前递增序列计数器。读取器在读取共享数据之前和之后读取序列计数器。如果序列计数器在任一时刻都是奇数,则写入器已获取锁,而数据正在被读取,它可能已更改。如果序列计数器不同,则写入器已更改数据,而它正在被读取。在这两种情况下,读取器只需重试 (使用循环) 直到它们在之前和之后读取到相同的偶数序列计数器。
💾 历史:语义在 2.5.59 版本中稳定下来,并且存在于 2.6.x 稳定内核系列中。顺序锁由 Stephen Hemminger 开发,最初称为 frlocks,基于 Andrea Arcangeli 的早期工作。第一个实现是在 x86-64 时间代码中,在那里需要与用户空间同步,而无法使用真正的锁。
⚲ API
👁 示例:mount_lock id,定义于 fs/namespace.c src
📖 参考资料
自旋锁或睡眠锁
[edit | edit source]在服务器上 在抢占式实时系统上 spinlock_t, raw_spinlock_t rt_mutex_base, rt_spin_lock, 睡眠 rwlock_t 自旋 睡眠 local_lock preempt_disable migrate_disable, rt_spin_lock, 睡眠
低级
[edit | edit source]编译器可能会优化掉或重新排序对变量的写操作,从而导致多个线程并发访问变量时出现意外行为。
⚲ API
- asm-generic/rwonce.h inc – 阻止编译器合并或重新获取读写操作。
- linux/compiler.hinc
- barrier id – 阻止编译器重新排序屏障周围的指令
- asm-generic/barrier.h inc – 通用屏障定义
- arch/x86/include/asm/barrier.h src – 强制严格的 CPU 排序
- mb id – 确保屏障之前的内存操作在屏障之后的任何内存操作开始之前完成
📚 进一步阅读
时间
[edit | edit source]⚲ UAPI
- uapi/linux/time.hinc
- timespec id — 纳秒级分辨率
- timeval id — 微秒级分辨率
- 时区id
- ...
- uapi/linux/time_types.hinc
- __kernel_timespec id — 纳秒级分辨率,用于系统调用
- ...
⚲ API
- linux/time.hinc
- tmid
- get_timespec64id
- ...
- linux/ktime.hinc
- ktime_t id — 用于内核时间值的纳秒标量表示
- ktime_subid
- ...
- linux/timekeeping.hinc
- linux/time64.hinc
- uapi/linux/rtc.hinc
- linux/jiffies.hinc
⚙️ 内部
📖 参考资料
...
[edit | edit source]⚙️ 锁定内部机制
📚 锁定引用
- 锁定 doc
- 锁类型及其规则 doc
- 睡眠锁 doc
- mutex id, rt_mutex id, semaphore id, rw_semaphore id, ww_mutex id, percpu_rw_semaphore id
- 在抢占式实时系统上:local_lock, spinlock_t, rwlock_t
- 自旋锁 doc:
- raw_spinlock_t, 位自旋锁
- 在非抢占式实时系统上:spinlock_t, rwlock_t
- 睡眠锁 doc
- 锁类型及其规则 doc
- 锁定不可靠指南 doc
- 同步(计算机科学)
- 同步原语
- 无滴答(全动态滴答), CONFIG_NO_HZ_FULL id
中断
[edit | edit source]中断是指由硬件或软件发出的信号,通知处理器需要立即处理的事件。中断提醒处理器发生了需要立即处理的紧急情况,因此需要中断处理器当前正在执行的代码。处理器会响应中断,暂停当前活动,保存状态,并执行一个称为中断处理程序(或中断服务例程,ISR)的函数来处理事件。中断是暂时的,中断处理程序完成后,处理器将恢复正常活动。
中断有两种类型:硬件中断和软件中断。硬件中断由设备使用,用于通知操作系统它们需要处理。例如,按下键盘上的键或移动鼠标会触发硬件中断,导致处理器读取按键或鼠标位置。与软件中断不同,硬件中断是异步的,可能在指令执行过程中发生,因此在编程时需要格外注意。启动硬件中断的动作称为中断请求 - IRQ(⚙️ do_IRQ id)。
软件中断是由处理器本身的异常情况或指令集中导致中断的特殊指令执行引起的。前者通常称为*陷阱*(⚙️ do_trap id)或*异常*,用于处理程序执行期间发生的错误或事件,这些事件足够异常,无法在程序本身内处理。例如,如果处理器的算术逻辑单元被命令将一个数字除以零,则此不可能的要求会导致*除零异常*(⚙️ X86_TRAP_DE id),可能导致计算机放弃计算或显示错误消息。软件中断指令的功能类似于子程序调用,并用于各种目的,例如请求低级系统软件(如设备驱动程序)的服务。例如,计算机通常使用软件中断指令与磁盘控制器通信,以请求读取或写入磁盘上的数据。
每个中断都有自己的中断处理程序。硬件中断的数量受处理器中断请求 (IRQ) 线的数量限制,但可能存在数百种不同的软件中断。
⚲ API
- /proc/interrupts
- man 1 irqtop – 用于显示内核中断信息的实用程序
- irqbalance – 在多处理器系统上将硬件中断分配到各个处理器
- 有许多方法可以请求 ISR,其中两种方法
- devm_request_threaded_irq id – 为具有线程化 ISR 的受管设备分配中断线的首选函数
- request_irq id,free_irq id – 用于添加和删除中断线处理程序的旧的常用函数
- linux/interrupt.h inc – 主要的中断支持头文件
- irqaction id – 包含处理程序函数
- linux/irq.hinc
- include/linux/irqflags.hinc
- linux/irqdesc.hinc
- linux/irqdomain.hinc
- irq_domain id – 硬件中断编号转换对象
- irq_domain_get_irq_dataid
- linux/msi.h inc – 消息信号中断
- 结构体的结构
- irq_desc id 是以下内容的容器
⚙️ 内部
- kernel/irq/settings.hsrc
- kernel/irqsrc
- ls /sys/kernel/debug/irq/domains/
- irq_chipid
📖 参考资料
👁 示例
- dummy_irq_chip id – 虚拟中断芯片实现
- lib/locking-selftest.csrc
IRQ 亲和性
[edit | edit source]⚲ API
- /proc/irq/default_smp_affinity
- /proc/irq/*/smp_affinity 和 /proc/irq/*/smp_affinity_list
常用类型和函数
- struct irq_affinity id – 用于自动 irq 亲和性分配的描述,参见 devm_platform_get_irqs_affinity id
- struct irq_affinity_desc id – 中断亲和性描述符,参见 irq_update_affinity_desc id,irq_create_affinity_masks id
- irq_set_affinityid
- irq_get_affinity_maskid
- irq_can_set_affinityid
- irq_set_affinity_hintid
- irqd_affinity_is_managedid
- irq_data_get_affinity_maskid
- irq_data_get_effective_affinity_maskid
- irq_data_update_effective_affinityid
- irq_set_affinity_notifierid
- irq_affinity_notifyid
- irq_chip_set_affinity_parentid
- irq_set_vcpu_affinityid
🛠️ 工具
- irqbalance – 将硬件中断分配到各个 CPU
...
[edit | edit source]📖 参考资料
📚 进一步阅读
- IDT – 中断描述符表
延迟工作
[edit | edit source]调度程序上下文
[edit | edit source]线程化 IRQ
[edit | edit source]⚲ API
devm_request_threaded_irq id,request_threaded_irq id
ISR 应返回 IRQ_WAKE_THREAD 以运行线程函数
⚙️ 内部
📖 参考资料
工作
[edit | edit source]工作是 workqueue 的包装器
⚲ API
- linux/workqueue.hinc
- work_struct id,INIT_WORK id,schedule_work id,
- delayed_work id,INIT_DELAYED_WORK id,schedule_delayed_work id,cancel_delayed_work_sync id
👁 示例用法 samples/ftrace/sample-trace-array.c src
⚙️ 内部机制:system_wq id
工作队列
[edit | edit source]⚲ API
⚙️ 内部
📖 参考资料
中断上下文
[edit | edit source]- linux/irq_work.h inc – 从硬中断上下文中将回调排队和运行的框架
定时器
[edit | edit source]软中断定时器
[edit | edit source]此定时器是用于具有节拍分辨率的定期任务的软中断
⚲ API
- linux/timer.hinc
- timer_list id, DEFINE_TIMER id, timer_setup id
- mod_timer id — 以节拍设置过期时间。
- del_timerid
⚙️ 内部
👁 示例
高分辨率定时器
[edit | edit source]⚲ API
- linux/hrtimer.hinc
- hrtimer id, hrtimer.function — 回调
- hrtimer_init id, hrtimer_cancel id
- hrtimer_start id 启动一个具有纳秒级分辨率的定时器
👁 示例 watchdog_enable id
⚙️ 内部
📚 HR 定时器参考
...
[edit | edit source]📚 定时器参考
任务
[edit | edit source]tasklet 是一个软中断,用于时间关键操作
⚲ API 已弃用,有利于线程化中断:devm_request_threaded_irq id
⚙️ 内部实现:tasklet_action_common id HI_SOFTIRQ, TASKLET_SOFTIRQ
软中断
[edit | edit source]软中断是内部系统工具,不应直接使用。使用 tasklet 或线程化中断
⚲ API
- cat /proc/softirqs
- open_softirq id 注册 softirq_action id
⚙️ 内部
⚲ API
📖 参考资料
CPU 特定
[edit | edit source]🖱️ GUI
- tuna – 用于调整运行进程的程序
⚲ API
- cat /proc/cpuinfo
- /sys/devices/system/cpu/
- /sys/cpu/
- /sys/fs/cgroup/cpu/
- grep -i cpu /proc/self/status
- rdmsr – 用于读取 CPU 机器特定寄存器 (MSR) 的工具
- man 1 lscpu – 显示有关 CPU 架构的信息
- linux/arch_topology.h inc – 架构特定的 CPU 拓扑信息
- linux/cpu.h inc – 通用 CPU 定义
- linux/cpu_cooling.hinc
- linux/cpu_pm.hinc
- linux/cpufeature.hinc
- linux/cpufreq.hinc
- linux/cpuhotplug.h inc – CPU 热插拔状态
- linux/cpuidle.h inc – 用于 CPU 空闲功耗管理的通用框架
- linux/peci-cpu.hinc
- linux/sched/cpufreq.h inc – cpufreq 驱动程序和调度程序之间的接口
- linux/sched/cputime.h inc – cputime 计费 API
⚙️ 内部
缓存
[edit | edit source]
⚙️ 内部
📖 参考资料
SMP
[edit | edit source]本章介绍 Linux 内核的多处理和多核方面。
Linux SMP 的关键概念和功能包括
- 对称性:在 SMP 系统中,所有处理器都被认为是相同的,没有硬件层次结构,与使用协处理器相反。
- 负载均衡:Linux 内核采用负载均衡机制将任务均匀地分配到可用 CPU 内核。这样可以防止任何一个内核不堪重负,而其他内核却闲置。
- 并行性:SMP 允许并行处理,其中多个线程或进程可以在不同的 CPU 内核上同时执行。这可以显着提高为利用多个线程而设计的应用程序的执行速度。
- 线程调度:Linux 内核调度程序负责确定哪些线程或进程在哪些 CPU 内核上运行以及运行多长时间。它旨在通过最大限度地减少竞争和最大限度地利用 CPU 来优化性能。
- 共享内存:在 SMP 系统中,所有 CPU 内核通常共享相同的物理内存空间。这允许在不同内核上运行的进程和线程更有效地通信和共享数据。
- NUMA – 非一致内存访问: 在较大的 SMP 系统中,由于内存库和处理器的物理排列,内存访问时间可能不一致。Linux 具有有效处理 NUMA 架构的机制,允许进程调度到更靠近其相关内存的 CPU 上。
- 缓存一致性: SMP 系统需要机制来确保所有 CPU 内核对内存的一致视图。缓存一致性协议确保对共享内存位置的更改正确传播到所有内核。
- 可扩展性: SMP 系统可以扩展到包含更多 CPU 内核,从而增强系统的整体计算能力。但是,随着内核数量的增加,可能会出现与内存访问、争用和内核之间通信相关的挑战。
- 内核和用户空间: 在用户空间运行的 Linux 应用程序可以利用 SMP,而无需了解底层硬件细节。内核处理 CPU 内核的管理和资源分配。
🗝️ 关键术语
- 亲和性是指将进程或线程分配到特定 CPU 内核。这有助于控制哪些 CPU 执行任务,通过减少内核之间的数据移动来提高性能。可以使用系统调用或命令来管理。亲和性可以用 CPU 位掩码表示: cpumask_t id 或 CPU 亲和性列表: cpulist_parse id.
⚲ API
ps -PLe
– 列出线程以及线程上次执行所在的处理器(第三列 PSR)。- man 1 taskset – 设置或检索进程的 CPU 亲和性
- man 2 getcpu – 确定调用线程正在运行的 CPU 和 NUMA 节点
- man 7 cpuset – 将进程限制在处理器和内存节点子集
- man 8 chcpu – 配置 CPU
- man 3 CPU_SET – 用于操作 CPU 集的宏
- grep Cpus_allowed /proc/self/status
- man 2 sched_setaffinity man 2 sched_getaffinity – 设置和获取线程的 CPU 亲和性掩码
- set_cpus_allowed_ptr id – 更改任务亲和性掩码的常用内核函数
- linux/smp.hinc
- linux/cpu.hinc
- linux/group_cpus.h inc: group_cpus_evenly id – 根据 NUMA/CPU 局部性均匀地对所有 CPU 进行分组
- linux/cpuset.h inc – cpuset 接口
- linux/cpu_rmap.h inc – CPU 亲和性反向映射支持
- linux/cpumask_types.hinc
- struct cpumask, cpumask_t id – CPU 位图,可能非常大
- cpumask_var_t id – 本地 cpumask 变量的类型,请参见 alloc_cpumask_var id, free_cpumask_var id.
- linux/cpumask.h inc – Cpumasks 提供一个位图,适合表示系统中的 CPU 集,每个 CPU 编号一个位位置
- asm-generic/percpu.hinc
- linux/percpu-defs.h inc – 每个 CPU 区域的基本定义
- linux/percpu.hinc
- linux/percpu-refcount.hinc
- linux/percpu-rwsem.hinc
- linux/preempt.hinc
- /sys/bus/cpu
- 每个 CPU local_lock
⚙️ 内部
- boot_cpu_init id 激活第一个 CPU
- smp_prepare_cpus id 在启动期间初始化其余 CPU
- cpuset_initid
- cpu_numberid
- cpus_mask id – task_struct id 的亲和性
- CONFIG_SMPid
- trace/events/percpu.hinc
- IPI – 处理器间中断
- trace/events/ipi.hinc
- kernel/irq/ipi.csrc
- ipi_send_single id, ipi_send_mask id ...
- drivers/base/cpu.c src – CPU 驱动程序模型子系统支持
- kernel/cpu.csrc
- smpboot
- lib/group_cpus.csrc
🛠️ 工具
- irqbalance – 将硬件中断分配到各个 CPU
- man 8 numactl – 控制进程或共享内存的 NUMA 策略
📖 参考资料
- cgroup v1 的 CPUSETS doc
- 命令行参数中的 CPU 列表 doc
- nohz_full 清理维护操作。cpumasks id 用于 housekeeping_nohz_full_setup id 中的 tick、wq、timer、rcu、misc 和 kthread
- isolcpus 清理维护操作。cpumasks id 用于 housekeeping_isolcpus_setup id 中的 tick、domain 和 managed_irq
📚 进一步阅读
- CPU 隔离技术现状,LPC'23
- CPU 分区
- 调度程序域 doc – 调度程序在调度程序域内平衡 CPU(调度组)
- CPU 隔离
- isolcpus @LKML
- nohz_full @LKML
- TuneD 插件的功能
CPU 热插拔
[edit | edit source]⚲ API
- linux/cpuhotplug.h inc – CPU 热插拔状态
⚙️ 内部
📖 参考资料
📚 进一步阅读
内存屏障
[edit | edit source]内存屏障 (MB) 是用于确保 SMP 环境中内存操作正确排序的同步机制。它们在维护不同 CPU 内核或处理器之间共享数据的 consistency 和正确性方面起着至关重要的作用。MB 阻止编译器或 CPU 对内存访问指令进行意外和潜在的 harmful 重排序,这会导致并发软件系统中的数据损坏和竞争条件。
⚲ API
⚙️ 内部
📖 参考资料
架构
[edit | edit source]Linux CPU 架构指的是与 Linux 操作系统兼容的各种中央处理器 (CPU) 类型。Linux 被设计为可在各种 CPU 架构上运行,使其能够在各种设备上使用,从智能手机到服务器和超级计算机。每种架构都有其独特的特性、优点和设计注意事项。
架构按系列(例如 x86、ARM)、字 或 长整型 大小(例如 CONFIG_32BIT id、CONFIG_64BIT id)进行分类。
一些针对不同 CPU 架构有不同实现的函数
- do_boot_cpu id > start_secondary id > cpu_init id
- setup_arch id、start_thread id、get_current id、current id
⚲ API
⚙️ 架构内部
📖 参考资料
📚 关于多任务、调度和 CPU 的进一步阅读
- ↑ a b Malte Skarupke。 "测量互斥锁、自旋锁以及 Linux 调度程序到底有多糟糕".